Skip to content

feat: optional strict response entities for the HTTP client - #1233

Draft
pjfanning wants to merge 3 commits into
apache:mainfrom
pjfanning:feat/client-strict-response-entities
Draft

feat: optional strict response entities for the HTTP client#1233
pjfanning wants to merge 3 commits into
apache:mainfrom
pjfanning:feat/client-strict-response-entities

Conversation

@pjfanning

@pjfanning pjfanning commented Aug 25, 2026

Copy link
Copy Markdown
Member

Motivation

Client responses are always dispatched with streamed entities, so applications have to consume or discard each response entity before the connection can be reused. Applications that only ever work with fully buffered responses end up calling toStrict on every response themselves.

Note that the client already produces HttpEntity.Strict opportunistically (when the whole body arrives together with the headers in one buffer), so "sometimes strict" is already the observable behaviour — this just makes it deterministic when asked for.

Modification

Two new settings in pekko.http.client:

# `off` by default - response entities stay streamed
strict-response-entity-timeout = off
strict-response-entity-max-bytes = ${pekko.http.parsing.max-to-strict-bytes}

When a timeout is configured, response entities are collected into an HttpEntity.Strict (reusing the existing impl.util.ToStrict stage) before the response leaves the connection layer. Entities that are already strict pass through untouched. The shared flow lives in StreamUtils.strictifyResponseEntities and takes a parallelism.

Two commits:

  1. HTTP/1.1 — applied in OutgoingConnectionBlueprint with parallelism 1. Because PoolInterface builds its connections through Http().outgoingConnectionUsingContext, this covers the connection-level API, the host-level API and singleRequest in one place. Responses on an HTTP/1.1 connection are sequential, so collecting one entity never holds up another.
  2. HTTP/2 — applied in Http2Blueprint.httpLayerClient with parallelism max-concurrent-streams, so a big response does not hold up smaller ones on other streams.

Caveats that apply to both, documented in reference.conf and in the client configuration docs:

  • the complete response body is buffered in memory;
  • a response that is not fully received in time fails with a TimeoutException, one that exceeds strict-response-entity-max-bytes with an EntityStreamException, and in both cases the connection is failed too, since the rest of the body cannot be skipped safely;
  • trailing headers of chunked responses are dropped, as they are with HttpEntity.toStrict.

HTTP/2 specific trade-offs

These are called out in a dedicated docs section, since they are not obvious:

  • Ordering changes. Responses are emitted in the order their entities complete, not the order their headers arrived. HTTP/2 responses are unordered by contract and have to be correlated via a RequestResponseAssociation, so this does not break the API, but it is an observable change. There is a test pinning this behaviour.
  • Memory. Worst case per connection is max-concurrent-streams × strict-response-entity-max-bytes — 256 × 8 MB with the defaults. Both settings should be tuned for the responses actually expected.
  • Blast radius of a failure. A response entity that times out or exceeds the maximum fails the whole connection, and with it every other stream in flight on it. There is no way to fail only the one response, since the remaining body cannot be skipped. With managedPersistentHttp2() the connection is re-established per max-persistent-attempts.
  • Flow control. Entity data is read from the network as fast as the peer sends it (up to the maximum) instead of at the pace the application consumes it, so HTTP/2 flow control no longer reflects application backpressure for response bodies.

Draft: opening for feedback on the setting names/shape and on whether the HTTP/2 trade-offs above are acceptable as they stand before this is marked ready.

Result

Both the HTTP/1.1 and the HTTP/2 client can be configured to hand out strict response entities. Default behaviour is unchanged: entities stay streamed.

Tests

  • sbt "http-core/testOnly org.apache.pekko.http.impl.engine.client.LowLevelOutgoingConnectionSpec" - pass (6 new cases: chunked, default, close-delimited, already-strict, over-limit, and streamed-by-default)
  • sbt "http-core/testOnly org.apache.pekko.http.impl.engine.client.HttpConfigurationSpec" - pass (2 new cases for defaults and pool propagation)
  • sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ClientSpec" - pass (4 new cases: collect entity, completion-order emission, over-limit, streamed-by-default)
  • sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ClientServerSpec org.apache.pekko.http.impl.engine.http2.Http2PersistentClientTlsSpec org.apache.pekko.http.impl.engine.http2.Http2PersistentClientPlaintextSpec" - pass
  • sbt http-core/mimaReportBinaryIssues - pass (no new filters needed)
  • sbt ++3.3.8 http-core/Test/compile http2-tests/Test/compile - pass
  • sbt docs/paradox - pass
  • scalafmt --list --mode diff-ref=upstream/main - no changes

References

None - makes client response entity handling configurable

Motivation:
Client responses are dispatched with streamed entities, so applications
always have to consume or discard the entity before the connection can be
reused. Applications that only ever work with fully buffered responses have
to call `toStrict` on every response themselves.

Modification:
Add `pekko.http.client.strict-response-entity-timeout` (`off` by default) and
`pekko.http.client.strict-response-entity-max-bytes` (8m by default). When a
timeout is configured, `OutgoingConnectionBlueprint` collects every response
entity into an `HttpEntity.Strict` before the response leaves the connection
layer, which also covers the connection pool behind the host-level and
request-level APIs. Entities that are already strict pass through untouched.

Result:
The HTTP/1.1 client can be configured to hand out strict response entities.
The default behaviour is unchanged: entities stay streamed.

Tests:
- sbt "http-core/testOnly org.apache.pekko.http.impl.engine.client.LowLevelOutgoingConnectionSpec" - pass
- sbt "http-core/testOnly org.apache.pekko.http.impl.engine.client.HttpConfigurationSpec" - pass
- sbt http-core/mimaReportBinaryIssues - pass
- sbt ++3.3.8 http-core/Test/compile - pass
- sbt docs/paradox - pass
- scalafmt --list --mode diff-ref=origin/main - no changes

References:
None - follow-up on making client response entity handling configurable
@pjfanning

pjfanning commented Aug 25, 2026

Copy link
Copy Markdown
Member Author

fyi @timw - based on #461 comments - still WIP and so far, it's mainly AI generated with little assessment from me

the HTTP/2 support should be added today but HTTP/2 is more complicated and will come with warnings

Motivation:
`pekko.http.client.strict-response-entity-timeout` only affected the HTTP/1.1
client, so HTTP/2 users could not get the same behaviour.

Modification:
Move the strictify flow into `StreamUtils.strictifyResponseEntities`, taking a
parallelism, and apply it in `Http2Blueprint.httpLayerClient` as well. The
HTTP/1.1 client keeps parallelism 1, since responses on a connection are
sequential anyway; the HTTP/2 client uses `max-concurrent-streams` so that a
big response does not delay smaller ones on other streams. Document the
HTTP/2 specific trade-offs in `reference.conf` and in the client configuration
docs: responses are emitted in the order their entities complete, worst case
memory is `max-concurrent-streams` * `strict-response-entity-max-bytes`, a
failing entity fails the whole connection with every stream in flight on it,
and entity data is read at the peer's pace rather than the application's.

Result:
The setting now covers both the HTTP/1.1 and the HTTP/2 client, with the
HTTP/2 caveats spelled out.

Tests:
- sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ClientSpec" - pass
- sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ClientServerSpec org.apache.pekko.http.impl.engine.http2.Http2PersistentClientTlsSpec org.apache.pekko.http.impl.engine.http2.Http2PersistentClientPlaintextSpec" - pass
- sbt "http-core/testOnly org.apache.pekko.http.impl.engine.client.LowLevelOutgoingConnectionSpec org.apache.pekko.http.impl.engine.client.HttpConfigurationSpec" - pass
- sbt http-core/mimaReportBinaryIssues - pass
- sbt ++3.3.8 http-core/Test/compile http2-tests/Test/compile - pass
- sbt docs/paradox - pass
- scalafmt --list --mode diff-ref=upstream/main - no changes

References:
None - follow-up to the HTTP/1.1 support in the previous commit
@pjfanning pjfanning changed the title feat: optional strict response entities for the HTTP/1.1 client feat: optional strict response entities for the HTTP client Aug 25, 2026
@timw

timw commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@pjfanning - looks promising.

I wonder if there'll be a performance benefit for clients - in my experience there's no benefit from applying toStrict in client code over simply processing a Chunked entity, but that could be because it's creating a separate stream, so I'm hopeful here.

I don't fully understand the pekko-http HTTP/2 code, but I don't see where this collects the HTTP/2 trailers into the trailer response attribute, since the ToStrict focuses on the entity data stream only, and not the Chunk level where trailers arrive in a LastChunk.

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.

2 participants