Skip to content

fix: reject a request body on a body-forbidden method (GET/HEAD/TRACE)#135

Open
OmarAlJarrah wants to merge 1 commit into
mainfrom
fix/request-body-on-forbidden-method
Open

fix: reject a request body on a body-forbidden method (GET/HEAD/TRACE)#135
OmarAlJarrah wants to merge 1 commit into
mainfrom
fix/request-body-on-forbidden-method

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Problem

The Request model made body optional for every method and ran no method/body compatibility check, so a GET (or HEAD/TRACE) request carrying a body was publicly constructible. The two reference transports then handled that case inconsistently, and neither was correct:

  • OkHttp passed the body to Request.Builder.method("GET", body), which throws IllegalArgumentException("method GET must not have a request body."). That unchecked exception escaped the transport's @Throws(IOException) contract and the retry pipeline.
  • JDK built a BodyPublisher from the body and then called builder.GET(), which ignores it. The body was silently dropped — and for a small body the eager publisher path had already drained a consume-once writeTo into a byte array that was then discarded.

This is the inverse of the body-less POST/PUT/PATCH case fixed in #82: a body present on a method that forbids one, rather than a body absent on a method that requires one.

Change

Reject at construction, so the two transports never have to disagree:

  • Method now carries a permitsRequestBody flag — false for GET, HEAD, TRACE, and CONNECT; true for the rest. The KDoc cites the relevant RFC 9110 sections.
  • Request.RequestBuilder.build() throws IllegalArgumentException when a body is set on a method whose permitsRequestBody is false, with a message that names the method and points at the body-permitting alternatives.
  • The JDK adapter now sends BodyPublishers.noBody() unconditionally for GET/HEAD/TRACE rather than adapting (and discarding) the body — there is no body to adapt, so nothing is consumed for nothing.
  • The OkHttp adapter is unchanged in behavior; its KDoc now records that the crash path is unreachable because core rejects the body before dispatch.

Rationale

Failing fast at the model boundary is the single consistent policy: OkHttp can no longer receive a body it would crash on, and the JDK transport no longer consumes a single-use body it never sends. The error is raised once, at build time, with a clear message — rather than surfacing differently (or not at all) depending on which transport is wired in.

Public API

One addition: the Method.permitsRequestBody getter. sdk-core api snapshot regenerated and committed.

Tests

  • sdk-core: build rejects a body on GET/HEAD/TRACE, allows it on POST/PUT/PATCH/DELETE/OPTIONS, allows a body-less forbidden method, and rejects a newBuilder that switches a body-carrying request to GET; plus a permitsRequestBody flag check in MethodTest.
  • Both transport suites: symmetric tests that a body on a forbidden method is rejected before dispatch and that a body-less GET dispatches with no body. The OkHttp async-adaptation-failure test now triggers the failure via a non-http(s) URL scheme (which OkHttp rejects) since GET-with-body can no longer be built.

Gated build commands run (all passed)

  • ./gradlew :sdk-core:test :sdk-core:ktlintCheck :sdk-core:detekt :sdk-core:apiCheck --no-daemon
  • ./gradlew :sdk-transport-okhttp:test :sdk-transport-okhttp:ktlintCheck :sdk-transport-okhttp:detekt :sdk-transport-okhttp:apiCheck --no-daemon
  • ./gradlew :sdk-transport-jdkhttp:test :sdk-transport-jdkhttp:ktlintCheck :sdk-transport-jdkhttp:apiCheck --no-daemon (detekt is skipped on this module)

Closes #117

A `Request` could be built with a body on a method that forbids one (GET,
HEAD, TRACE). The two transports then handled it inconsistently and neither
was correct:

- OkHttp's `Request.Builder.method("GET", body)` throws
  `IllegalArgumentException("method GET must not have a request body.")`.
  That unchecked exception escaped the transport's `@Throws(IOException)`
  contract (and the retry pipeline).
- The JDK transport built a `BodyPublisher` from the body and then called
  `builder.GET()`, which ignores it. The body was silently dropped — and for
  a small body the eager publisher path had already drained a consume-once
  `writeTo` into a byte array that was then discarded.

Pick one consistent policy and reject at construction. `Method` now carries a
`permitsRequestBody` flag (`false` for GET/HEAD/TRACE/CONNECT), and
`Request.RequestBuilder.build()` throws `IllegalArgumentException` when a body
is set on a method that forbids one. Failing fast at the model boundary keeps
both transports from diverging: OkHttp can no longer receive a body it would
crash on, and the JDK transport no longer consumes a body it never sends. The
JDK adapter now sends `noBody()` unconditionally for GET/HEAD/TRACE instead of
adapting (and discarding) the publisher.

Adds symmetric coverage in both transport suites plus core tests for the new
validation and the `permitsRequestBody` flag. The new `Method.permitsRequestBody`
getter is the only public-API addition (api snapshot regenerated).

Closes #117
@OmarAlJarrah OmarAlJarrah changed the title Reject a request body on a body-forbidden method (GET/HEAD/TRACE) fix: reject a request body on a body-forbidden method (GET/HEAD/TRACE) Jun 17, 2026
@OmarAlJarrah

Copy link
Copy Markdown
Member Author

Nice cleanup. Centralizing the body-on-forbidden-method check at RequestBuilder.build() is the right move — failing fast at construction beats the previous behavior where OkHttp threw and the JDK transport silently dropped the body, and the transport adapters read much cleaner now relying on the guarantee. Approving with one small doc nit.

Worth double-checking

  • Docs enumerate only GET/HEAD/TRACE as body-forbidden, but CONNECT is also rejected (sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/request/Method.kt:18-23). CONNECT is declared with permitsRequestBody = false, so build() now rejects a body on a CONNECT request too — but the KDoc on Method.permitsRequestBody, the KDoc on Request.body (Request.kt:40-42), and the build() @throws/inline KDoc (Request.kt:252-261) all list only GET/HEAD/TRACE. The new MethodTest (titled permitsRequestBody is false only for GET HEAD TRACE and CONNECT) already asserts CONNECT, so the code and tests agree — it's just the prose that's stale. Could you fold CONNECT into those doc enumerations so the documented contract matches what build() actually enforces? This is also a quiet behavior change for CONNECT+body (previously reached the transport, now rejected at build); worth a line of mention even though both transports reject CONNECT anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transports mishandle a request body set on a body-forbidden method (GET/HEAD)

1 participant