Restore 1.x API compatibility while keeping 1.5 hardening - #629
Conversation
rowan-m
left a comment
There was a problem hiding this comment.
Thank you for jumping on this.
Quick initial thoughts:
I think type hints can stay because the minimum PHP version remains at 8.4.
The build fails due to a strict composer check as there are changes to composer.json without an updated lock file - even though the changes don't affect the dependencies. Not sure on the best approach there... probably just update the lock file as I should do a separate update of the dependencies after this PR.
|
Thanks for the feedback @rowan-m. That makes sense. Since the minimum PHP version is still 8.4, I agree that the type hints can stay. The Composer failure was my oversight; I hadn’t run composer update after the composer.json changes, so the lock file was out of sync. I’ve refreshed it now and rerun the checks, so that part should be sorted. |
acoulton
left a comment
There was a problem hiding this comment.
Thanks - I had a quick look through and noticed a few things that don't look quite right to me.
@rowan-m: I think type hints can stay because the minimum PHP version remains at 8.4.
It depends which type hints you mean?
Return type hints are a definite BC break.
Parameter type hints will potentially break people's runtime usage. Whether you consider that a BC break depends how strictly you think people should have been following/checking the phpdoc type hints in the past (and whether they have declare(strict_types=1) in their calling code. I'd suggest as a minimum verify should take a nullable string.
Property type hints may break people's usage if they are not strictly following the phpdoc of constructor params, as above.
IMO however this PR goes too far in trying to coalesce parameter/property values that would have been invalid/cause errors in the past.
|
Hi @acoulton, you were right on this one. I missed the fact that the goal here should be to preserve the 1.4.x behavior, not to make the API “more helpful” by coercing inputs or changing how errors surface. I wasn’t trying to change the behavior for the sake of it. My thinking was that the stricter typing and the extra checks would make the code safer and easier to use, but in doing that, I crossed the line from compatibility into behavior changes. That was the wrong tradeoff for this release. I’ve been working through the fixes since your comment, and I’m updating the implementation now so it matches the original behavior more closely while still keeping the reliability improvements that don’t affect BC. I’ll push the changes shortly. Thanks for the comments. |
Co-authored-by: Copilot <copilot@github.com>
|
Thanks for the feedback — I updated the PR to restore 1.4.x compatibility while keeping the safe reliability fixes from 1.5.0. |
|
The problem is that the typehints broke semantic versioning. v1.5 really should've been v2.0.. |
|
Sorry for the "bump-like" comment, but is there any way to move this forward? Are there some blockers? |
…ility release Resolves the BC breaks reported in google#628 by restoring the 1.4.2 public API byte-for-byte, while preserving 1.5.0's security hardening. The forward, strict/typed surface belongs in a separate 2.0.0 (major) release. Restored to 1.4.2 (public API diff against the 1.4.2 tag is empty except VERSION): - untyped public signatures + phpdoc (no native param/return types) - non-readonly Response / RequestParameters - Curl and Socket wrapper classes and their DI constructors (?Curl / ?Socket) - no return type on the RequestMethod::submit() interface - removed the scalar-coercion helpers that caused a verify() fail-open on misconfigured setExpected*() values; raw assignment restored (floatval kept only in setScoreThreshold, as in 1.4.2) Hardening kept from 1.5.0 (internal, non-breaking): - TLS peer + hostname verification and timeout in Post (moved to ssl context) - CURLOPT_TIMEOUT in CurlPost; fail-closed is_string guard in verify() - URL validation, HTTP/1.1 acceptance and body-part guard in SocketPost - avoids curl_close() entirely, fixing the PHP 8.5 deprecation (google#630/google#632) on 1.x Tooling: phpstan level max 0 errors, phpunit 52 passing, php-cs-fixer clean. phpstan.neon: treatPhpDocTypesAsCertain:false so defensive runtime guards on the untyped-but-phpdoc'd public API pass without adding BC-breaking native types.
fd57939 to
76bd199
Compare
|
Thanks @acoulton, @rowan-m and @jonnott for the review — and sorry for the delay. You were right that my first pass over-corrected: it coerced parameter/property values, What the latest commit delivers The goal is now one sentence: the public API is byte-for-byte compatible with the 1.4.2
(One transparency note: Next steps
Very open to other opinions on any of this at any point — happy to adjust the numbering, |
|
@SNO7E-G thanks for all the work on this. Obviously it really depends what @rowan-m thinks, as I'm just a user, not a maintainer here. But IMO this PR is very hard to review, because it is partially unwinding changes that were already merged and released, but also introducing or keeping new things. This means there are lots of things in the diff that look odd, but may be correct. Without a line-by-line comparison between 1.4.2 and the merged result of this PR, it's impossible to tell whether it correctly fixes the BC breaks that were introduced in 1.5.0 without introducing any other problems. I don't really have time to do that. If it was my project, I'd be strongly encouraging splitting this into separate things:
|
Adds the assertion that CurlPost sets CURLOPT_TIMEOUT and CURLOPT_SSL_VERIFYPEER, which was the one part of the hardening left without direct coverage, and records in phpstan.neon why phpdoc types are not treated as certain: a custom RequestMethod implementation is not bound by the interface phpdoc, so the runtime guards on what it returns are meaningful rather than redundant. Brings this branch in line with the smaller PRs it is being split into.
Choosing and tagging a release is the maintainers' call, so this branch no longer bumps the VERSION constant. It stays at the value from 1.4.2, the release whose API is being restored. Also records in phpstan.neon why phpdoc types are not treated as certain: a custom RequestMethod implementation is not bound by the interface phpdoc, so the runtime guards on what it returns are meaningful rather than redundant.
|
Thanks, @acoulton — that's really helpful feedback — and a fair criticism. You're right that the PR is doing two things at once, and that makes it hard to review even where the individual changes are fine. I'm reworking it along the lines you suggested: first a PR that just restores the 1.4.2 API, so Two things I ran into that are worth mentioning in advance. 1.4.2 predates the phpstan level max setup that's now in CI and reports quite a few pre-existing errors, several of which can't be fixed with annotations alone — so the restore PR carries a phpstan baseline rather than changing code in a revert, and the later PRs empty it again. And while going through the Thanks again for taking the time to look at it. |
Response::fromJson() compared the success field loosely. In PHP
`true == "false"` is true, so a body of {"success": "false"} verified as a
success, as would any non-empty string or non-zero number. The comparison
is now strict, with tests for the string and integer forms. Google sends a
real boolean over TLS, but this was the one place a response that is not
what the client expects could turn into a pass rather than a failure.
Socket::fsockopen() applies the connect timeout to reads as well. The
timeout passed to fsockopen only covers connecting, and reads otherwise
fall back to default_socket_timeout, which a host is free to set so that it
never times out.
Socket::fgets() returns false at end of file again and Curl::exec() passes
the curl_exec result through unchanged, rather than mapping either to
something friendlier. Both are public classes that 1.4.2 shipped, so
anything driving them directly should see exactly the old behaviour; the
invalid-handle checks stay, since those only replace a TypeError. CurlPost
now treats a non-string result as a failed request instead.
Closes #628
Thanks again, @acoulton and @mbabker, for raising and detailing the BC concerns.
This PR focuses on restoring 1.x compatibility expectations while preserving the useful reliability and hardening work already introduced.
What this PR does
1) Restores 1.x public API compatibility
readonlybehavior from public non-final DTOs (Response,RequestParameters) so extension/mocking remains possible.RequestMethod::submit()without a native return type (1.x-compatible contract).ReCaptcha::verify()to safely handle non-string request-method responses (E_BAD_RESPONSE).verify(...).new CurlPost(null, $url)new SocketPost(null, $url)CurlandSocketwrapper classes as public compatibility surfaces.2) Fixes compatibility/quality edge cases found during review
SocketPostearly-return path to ensure handle close when timeout setup fails.Response::fromJson(...)semantics needed for 1.x consumers.3) Keeps the good hardening/reliability improvements
SocketPostHTTP/1.1 handling remains in place.ReCaptcharemains."0"response handling remains valid.Tests and coverage updates
Added/updated regression tests for:
RequestMethodimplementation without native return typeRequestMethodresponse handlingverify(null)compatibility behavior"0"response handlingResponseandRequestParametersCurl/Socket)Documentation updates
Validation run
vendor/bin/phpunit --no-coveragevendor/bin/phpstanvendor/bin/php-cs-fixer check --using-cache=noOnly caution (not new, but important):
Note to @rowan-m
I already bumped the client version string to
1.5.1in this work to keep the release path straightforward and avoid extra follow-up edits.If that decision should stay maintainer-only, I’m sorry — please feel free to adjust/revert that part as you prefer.