feat: Pluggable transport interface + NetHTTP default transport. - #357
feat: Pluggable transport interface + NetHTTP default transport.#357sichanyoo wants to merge 7 commits into
Conversation
jterapin
left a comment
There was a problem hiding this comment.
This is a meaningful step toward a cleaner transport boundary, and I appreciate how much groundwork this PR is laying. I still have a couple blocking concerns around abort/session ownership and how explicitly we define the SendHandler contract (we may need to regroup for team discussion), but I think the overall direction is promising.
It would also be helpful to include a PR description for review context. As a note, I’ll be reviewing the specs in a second pass.
| # Raised when an operation is not supported by a transport or its stream. | ||
| # | ||
| # For example, HTTP/1.1 streams cannot be written to after the request has | ||
| # been transmitted, so {NetHTTP::Stream#write} and | ||
| # {NetHTTP::Stream#close_write} raise this error. | ||
| class NotSupportedError < StandardError; end |
There was a problem hiding this comment.
Is NotSupportedError intended to be part of the shared transport/stream contract, or is it really specific to the NetHTTP adapter? I also think this belongs in Errors module.
| # Net::HTTP defaults verify_mode to VERIFY_PEER; set it explicitly so | ||
| # a VERIFY_NONE request (ssl_verify_peer: false) is actually honored | ||
| # instead of silently falling back to verification. | ||
| http.verify_mode = http_verify_mode |
| module Smithy | ||
| module Client | ||
| module NetHTTP | ||
| # An HTTP/1.1 stream backed by +Net::HTTP+, implementing the stream |
There was a problem hiding this comment.
I think some of the details here can be cut down to the most relevant parts.
| # via the broad StandardError rescue) so it is never confused with a real | ||
| # networking failure. Not part of the stream contract. | ||
| # @api private | ||
| class Aborted < StandardError; end |
There was a problem hiding this comment.
Aborted feels a little bare here and easy to confuse with a user-visible cancellation path. I’d consider a more specific internal name.
|
|
||
| # Net::HTTP applies its default Content-Type while sending the | ||
| # request, which happens during this first resume. | ||
| Thread.current[:net_http_skip_default_content_type] = true |
There was a problem hiding this comment.
I believe newer Net::HTTP versions fixed this behavior, and we also carry patching around related cases on our side. I will suggest that you double-check whether this comment still reflects the current runtime behavior.
| # The generic transport plugin. It: | ||
| # | ||
| # * registers the transport-agnostic +:send+ handler ({SendHandler}), | ||
| # * resolves the +:transport+ used to send requests (the documented swap | ||
| # point), and | ||
| # * defines the transport-agnostic client options (connect/read timeouts, | ||
| # proxy, TLS verification and trust store, wire trace) that every | ||
| # built-in transport honors. | ||
| # | ||
| # These options use transport-neutral names and are forwarded to the | ||
| # default transport at construction. Transport-specific knobs (for example | ||
| # Net::HTTP's keep-alive/continue timeouts or client certificates) are not | ||
| # exposed as client options: a customer who needs them constructs a | ||
| # transport instance directly and passes it as +:transport+ (see | ||
| # {Smithy::Client::NetHTTP::Transport}). A customer-supplied transport is | ||
| # used as-is, so these options do not apply to it. |
There was a problem hiding this comment.
| # The generic transport plugin. It: | |
| # | |
| # * registers the transport-agnostic +:send+ handler ({SendHandler}), | |
| # * resolves the +:transport+ used to send requests (the documented swap | |
| # point), and | |
| # * defines the transport-agnostic client options (connect/read timeouts, | |
| # proxy, TLS verification and trust store, wire trace) that every | |
| # built-in transport honors. | |
| # | |
| # These options use transport-neutral names and are forwarded to the | |
| # default transport at construction. Transport-specific knobs (for example | |
| # Net::HTTP's keep-alive/continue timeouts or client certificates) are not | |
| # exposed as client options: a customer who needs them constructs a | |
| # transport instance directly and passes it as +:transport+ (see | |
| # {Smithy::Client::NetHTTP::Transport}). A customer-supplied transport is | |
| # used as-is, so these options do not apply to it. | |
| # Generic transport plugin for Smithy clients. It: | |
| # | |
| # * registers the shared +:send+ handler ({SendHandler}), | |
| # * refines the common client transport options, and | |
| # * constructs the default transport when one is not supplied explicitly. | |
| # | |
| # These client options cover the shared transport settings exposed on | |
| # `Client.new(...)`. Transport-specific options remain adapter-specific and | |
| # must be configured on the transport instance itself. | |
| # If a caller supplies a transport via +:transport+, that instance | |
| # is used directly. |
Tightened it up and using caller instead of customer.
| # transport. Keeping them in one list keeps the option definitions and | ||
| # the forwarding in sync: add a name here and define the matching | ||
| # +option(...)+ below and it is forwarded automatically. | ||
| TRANSPORT_OPTIONS = %i[ |
There was a problem hiding this comment.
I think there’s a bit of a disconnect here now that the transport config surface and ConnectionPool::OPTIONS are maintained separately.
Since Transport#pool_options is doing a handwritten translation, it feels easy for the two lists to drift. We can use this const as a reference point for default transport options?
|
|
||
| option( | ||
| :connect_timeout, | ||
| default: nil, |
There was a problem hiding this comment.
Shouldn't we document net-http transport defaults here? How will the user find out the defaults if they are using default net-http. Same question for others listed here.
| transport-agnostic client options. Supply a custom object responding to | ||
| `#transmit(request)` (returning a stream) to swap the transport, or a | ||
| directly-constructed `NetHTTP::Transport` to set Net::HTTP-specific knobs. A | ||
| customer-supplied transport instance is used as-is. |
There was a problem hiding this comment.
I would suggest that you update the wording, "customer-supplied" here
| # This handler is transport-agnostic: it depends only on the stream contract | ||
| # (+#response_headers+, +#each_chunk+, +#abort+) and never on a concrete | ||
| # transport. | ||
| # Protocol-specific concerns - how blocking is implemented, connection | ||
| # pooling, HTTP/1.1 body-truncation detection - live in the transport and | ||
| # its stream. This handler only decides *when* to block (immediately, for | ||
| # plain request/response operations) and bridges the pulled bytes onto the | ||
| # push-based {Http::Response} the rest of the stack consumes. | ||
| # @api private |
There was a problem hiding this comment.
I don’t think I’m asking for a redesign in this PR, but I do want to push on the SendHandler contract a bit more before approval.
My concern is that the current docs describe this boundary as more transport-agnostic than the code actually is. The handler is not tied to a concrete adapter like Net::HTTP, but it is still defining a fairly specific staged streaming model that transports must fit:
transmitreturns before body consumption is complete- headers are available as a distinct phase
- body is consumed later via ordered chunk reads
- a live handle supports abort during that exchange
So I think the distinction here is less “agnostic” and more “adapter-independent but contract-shaping.” Alternate transports can plug in, but only if they adapt themselves to this stream model.
Right now it feels like we are making every transport look like a staged pull stream, which means more push/event-style clients have to reshape themselves to fit us. That makes the transport boundary feel less swappable than the docs suggest, and it leaves implementers to infer what timing, ordering, and cancelation rules the SDK is actually expecting.
One idea I keep coming back to is whether more of this shaping should live inside the transport instead. In that model, transports could keep their own internal execution model, but turn it into the SDK’s expected response lifecycle at the boundary: headers first, then ordered body delivery, then one terminal done/error. The thing returned from transmit could then stay smaller and focus mostly on live control like abort, plus write-side methods if we need duplex.
I’m not settled on this approach, but I wanted to float it as a possible direction.
Issue #, if available:
5196
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.