fix(netty): preserve non-POST redirect methods - #2325
Conversation
RFC 9110 scopes the historical 301 and 302 POST-to-GET rewrite to POST. AHC applied it to other methods, silently dropping content from PUT, PATCH, DELETE, and extension requests. Retain the established POST behavior while repeating those non-POST requests with their bodies. Cross-origin redirects still strip credentials even though request content is replayed. OpenAI Codex on behalf of Matthias Kurz. Co-Authored-By: OpenAI Codex <codex@openai.com>
Keep request content whenever a redirect preserves its method, except for the explicit body-dropping semantics of 303. This covers GET, HEAD, OPTIONS, and caller-added redirect statuses while retaining the historical POST rewrite for 301 and non-strict 302. Co-Authored-By: OpenAI Codex <codex@openai.com>
Request builders can retain lower-priority body representations. Reuse the outbound body precedence for redirect checks so stale multipart streams do not reject replayable byte-array redirects. Co-Authored-By: OpenAI Codex <codex@openai.com>
Reject selected raw streams and InputStream body generators that declare no mark/reset support before opening the redirect target. The write-time reset remains the final check for streams that advertise support but cannot actually reset after their first send. Co-Authored-By: OpenAI Codex <codex@openai.com>
Prove that a cross-origin PUT redirect receives credentials only on its original leg while preserving the method, content type, and body on the target leg. Also cover body preservation when the redirect changes the hostname without changing the server. Co-Authored-By: OpenAI Codex <codex@openai.com>
|
@hyperxpro looking forward to your review ;) |
Sorry took longer than expected |
An early redirect can arrive before an Expect: 100-continue body is written. Only require reset support after stream transmission starts, matching the existing write-time guard and allowing the target to read an untouched stream for the first time. Cover raw streams and InputStreamBodyGenerator with a 307 origin that receives no body before redirecting. Multipart validation retains its separate rules because it does not update the stream-consumed flag. Fix the deferred-stream regression reported in PR AsyncHttpClient#2325. OpenAI Codex on behalf of Matthias Kurz. Co-Authored-By: OpenAI Codex <codex@openai.com>
No worries at all - thanks for taking the time to test both stream representations and catch this. |
|
@hyperxpro assuming this gets merged, would it be possible to cut a new release so I can use it downstream? Thanks! |
I plan to cut release next week as there are some fixes I need to perform. Is it possible to use SNAPSHOT till then? |
|
Thanks a lot! |
Next week is ok, thanks! |
Summary
Problem
Redirect30xInterceptorapplied the historical POST-to-GET behavior for 301 and non-strict 302 responses to every method other than GET, HEAD, and OPTIONS. A PUT, PATCH, DELETE, or extension-method request therefore became a bodyless GET after either redirect.The body decision was also coupled to a fixed list of methods and status codes. GET, HEAD, and OPTIONS requests can mechanically carry bodies in AHC, but those bodies were dropped on 301 and 302 even though the methods were retained. A caller-added status in the public mutable
REDIRECT_STATUSESset likewise retained the method while silently dropping its body.RFC 9110 sections 15.4.2 and 15.4.3 scope the compatibility allowance to changing POST to GET. They do not permit rewriting every other method in the same way. RFC 10008 section 2.5 explicitly requires QUERY not to use the POST exceptions.
Change
Apply the legacy 301/302 rewrite only when the original method is POST. Derive body handling from the method decision: keep the body whenever the method is preserved, except for 303, which drops the body regardless. This differs deliberately from checking whether a request is "not POST": a POST receiving a caller-added status such as 300 also keeps its method and must therefore keep its body.
The resulting behavior is:
This is a deliberate compatibility change. It corrects the standards scope and removes method-preserving, body-dropping combinations while retaining the long-established POST behavior for 301 and non-strict 302.
Redirect-body validation
The replay checks now use one private
BodyRepresentationselection that mirrorsNettyRequestFactory.body()precedence. This matters because some request-builder setters leave lower-priority representations in place. Validation now examines only the body AHC selected for transmission; for example, a stale multipartInputStreamPartno longer rejects a redirect whose selected body is a byte array.Selected raw
InputStreambodies andInputStreamBodyGeneratorinstances that report no mark/reset support are rejected before AHC connects to the redirect target only if the stream has already been consumed. An early redirect in response toExpect: 100-continuecan leave the stream untouched, so the target can still receive its first transmission without reset support. This preflight is intentionally partial: a stream may report mark support yet be closed or fail to reset after the first send. The existing write-time replay guard remains the final authority for those cases. A genericBodyGeneratorcan also produce an unknown-length or non-repeatable body, but discovering that would require calling its one-shotcreateBody()early, so its existing write-time behavior is unchanged.Redirect security
For a cross-origin 301 or 302, newly preserved request content is replayed to the redirect target. This follows the body-replay trust model AHC already uses for strict 302, 307, and 308. Existing redirect security continues to strip Authorization, Proxy-Authorization, Realm credentials, user-supplied Cookie headers, and Cookie objects when the origin changes. Tests assert that credentials reach the original server, do not reach the target, and that the method, content type, and body reach the target intact.
The same consideration applies to HTTPS-to-HTTP redirects: this pull request can replay content on 301 or 302 that the previous method/body rewrite discarded. Gating only
keepBodyon a scheme downgrade would preserve the method while silently deleting its payload, reproducing the data-corruption shape this change removes. If AHC adopts a downgrade restriction, it should refuse the redirect itself and apply uniformly to all keep-body statuses, including strict 302, 307, and 308. That transport-policy decision is left to a focused follow-up; this pull request does not alter AHC's existing downgrade policy.History checked
The broad conversion is established behavior rather than a recent accident. Issue #989 requested browser-compatible 301 handling, issue #1042 retained the body drop for POST, and pull request #1736 later exempted HEAD and OPTIONS from method rewriting. This change retains the established POST rule while making body preservation follow the resulting method decision. A search did not find an existing issue or pull request specifically correcting PUT, PATCH, DELETE, extension methods, or caller-added redirect statuses.
Compatibility
There is no public API change.
The following redirect behavior changes intentionally:
InputStreamorInputStreamBodyGeneratorthat has already been consumed and lacks mark/reset support now fails before connecting to the redirect target. For newly preserved non-POST 301/302 requests, this replaces the previous silent success as a bodyless GET with an explicit replay failure. Existing keep-body redirects fail earlier and use the new redirect-level error message. Untouched streams deferred byExpect: 100-continueremain usable at the redirect target. Streams that advertise mark support but cannot actually reset still fail at write time.POST on 301 and non-strict 302, strict-302 method policy, 303 method policy, and 307/308 method policy remain unchanged.
AI disclosure
OpenAI Codex on behalf of Matthias Kurz. The commits include
Co-Authored-By: OpenAI Codex <codex@openai.com>perAGENTS.md.Test plan
InputStreamPartfalse rejection before validation was aligned with outbound-body precedence.InputStreamandInputStreamBodyGeneratorbodies../mvnw -pl client -Dtest=RedirectBodyTest,RedirectCredentialSecurityTest teston JDK 11: 73 tests passed../mvnw clean verifyon JDK 11: BUILD SUCCESS (full reactor, including tests, Javadocs, artifact signing, coverage, and Revapi).Generated with OpenAI Codex.