Skip to content

feat: Pluggable transport interface + NetHTTP default transport. - #357

Open
sichanyoo wants to merge 7 commits into
mainfrom
pluggable-transport
Open

feat: Pluggable transport interface + NetHTTP default transport.#357
sichanyoo wants to merge 7 commits into
mainfrom
pluggable-transport

Conversation

@sichanyoo

@sichanyoo sichanyoo commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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.

@sichanyoo
sichanyoo requested a review from a team as a code owner August 24, 2026 23:01

@jterapin jterapin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +10
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix!

module Smithy
module Client
module NetHTTP
# An HTTP/1.1 stream backed by +Net::HTTP+, implementing the stream

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +9 to +24
# 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 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[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest that you update the wording, "customer-supplied" here

Comment on lines +9 to +17
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • transmit returns 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.

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