From 9cb3e7a3f81095cb8db7d993fa2e116a91c9a1e0 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 20 Aug 2026 12:26:34 +1200 Subject: [PATCH 1/5] Revise documentation guides Assisted-By: devx/eb0838cf-417f-4559-90ed-e9a0d84bfd2d --- guides/getting-started/readme.md | 173 +++++++++++++++++----------- guides/testing/readme.md | 192 +++++++++++++++++++++++-------- 2 files changed, 250 insertions(+), 115 deletions(-) diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 3f674834..888d2101 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -1,6 +1,6 @@ # Getting Started -This guide explains how to get started with `Async::HTTP`. +This guide explains how to make HTTP requests and serve HTTP responses with `Async::HTTP`. ## Installation @@ -12,137 +12,172 @@ $ bundle add async-http ## Core Concepts -- {ruby Async::HTTP::Client} is the main class for making HTTP requests. -- {ruby Async::HTTP::Internet} provides a simple interface for making requests to any server "on the internet". -- {ruby Async::HTTP::Server} is the main class for handling HTTP requests. -- {ruby Async::HTTP::Endpoint} can parse HTTP URLs in order to create a client or server. -- [`protocol-http`](https://github.com/socketry/protocol-http) provides the abstract HTTP protocol interfaces. +`Async::HTTP` provides several interfaces for different kinds of HTTP applications: -## Usage +- {ruby Async::HTTP::Internet} makes requests to arbitrary hosts and manages a client for each remote endpoint. +- {ruby Async::HTTP::Client} manages persistent connections to a specific endpoint. +- {ruby Async::HTTP::Server} accepts connections and dispatches requests to an HTTP application. +- {ruby Async::HTTP::Endpoint} describes how a client connects or a server listens, including the URL, protocol, and TLS configuration. +- [`protocol-http`](https://github.com/socketry/protocol-http) provides the shared request, response, header, and body interfaces. -### Making a Request +Use `Internet` for general-purpose requests to different hosts. Use `Client` when your application repeatedly communicates with one endpoint or needs endpoint-specific configuration. -To make a request, use {ruby Async::HTTP::Internet} and call the appropriate method: +## Making a Request + +The shared {ruby Async::HTTP::Internet} instance provides a convenient starting point. Run asynchronous HTTP operations inside `Sync`, which creates or reuses the event loop while returning the block result directly: ~~~ ruby -require 'async/http/internet/instance' +require "async/http/internet/instance" Sync do Async::HTTP::Internet.get("https://httpbin.org/get") do |response| + puts "Status: #{response.status}" puts response.read end end ~~~ -The following methods are supported: +Passing a block automatically closes the response when the block exits, including when an exception is raised. Responses are streamed, so callers that do not use the block form must close the response explicitly. ~~~ ruby -Async::HTTP::Internet.methods(false) -# => [:patch, :options, :connect, :post, :get, :delete, :head, :trace, :put] +require "async/http/internet/instance" + +Sync do + response = Async::HTTP::Internet.get("https://httpbin.org/get") + puts response.read +ensure + response&.close +end ~~~ -Using a block will automatically close the response when the block completes. If you want to keep the response open, you can manage it manually: +Convenience methods are provided for `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`, and `QUERY` requests. + +### Connection Persistence + +`Internet` creates a {ruby Async::HTTP::Client} for each remote endpoint and reuses its persistent connections. The underlying async pools are bound to the event loop and are closed when that event loop exits. + +An explicitly created `Internet` can also be closed early when an application wants to release all cached clients before the event loop exits: ~~~ ruby -require 'async/http/internet/instance' +require "async/http/internet" Sync do - response = Async::HTTP::Internet.get("https://httpbin.org/get") - puts response.read + internet = Async::HTTP::Internet.new + + internet.get("https://example.com") do |response| + puts response.status + end ensure - response&.close + internet&.close end ~~~ -As responses are streamed, you must ensure it is closed when you are finished with it. +## Working with Responses -#### Persistence +A response contains a status, headers, and a streaming body. Check the status before processing content, and use header names in lower case: -By default, {ruby Async::HTTP::Internet} will create a {ruby Async::HTTP::Client} for each remote host you communicate with, and will keep those connections open for as long as possible. This is useful for reducing the latency of subsequent requests to the same host. When you exit the event loop, the connections will be closed automatically. +~~~ ruby +require "async/http/internet/instance" + +Sync do + Async::HTTP::Internet.get("https://httpbin.org/json") do |response| + if response.success? + puts response.headers["content-type"] + puts response.read + else + warn "Request failed with status #{response.status}." + end + end +end +~~~ + +For larger responses, process the body incrementally rather than reading it into one string. See the `protocol-http` message body documentation for the complete body interface. ### Downloading a File +Use `response.save` to stream a response directly to a file: + ~~~ ruby -require 'async/http/internet/instance' +require "async/http/internet/instance" Sync do - # Issue a GET request to Google: - response = Async::HTTP::Internet.get("https://www.google.com/search?q=kittens") - - # Save the response body to a local file: - response.save("/tmp/search.html") -ensure - response&.close + Async::HTTP::Internet.get("https://example.com/archive.zip") do |response| + raise "Download failed with status #{response.status}." unless response.success? + + response.save("archive.zip") + end end ~~~ -### Posting Data +## Posting JSON -To post data, use the `post` method: +Pass headers and a body after the request target. The body may be a string or a compatible `protocol-http` body object. ~~~ ruby -require 'async/http/internet/instance' +require "async/http/internet/instance" +require "json" -data = {'life' => 42} +data = {life: 42} +headers = [ + ["accept", "application/json"], + ["content-type", "application/json"], +] Sync do - # Prepare the request: - headers = [['accept', 'application/json']] - body = JSON.dump(data) - - # Issues a POST request: - response = Async::HTTP::Internet.post("https://httpbin.org/anything", headers, body) - - # Save the response body to a local file: - pp JSON.parse(response.read) -ensure - response&.close + Async::HTTP::Internet.post("https://httpbin.org/anything", headers, JSON.dump(data)) do |response| + raise "Request failed with status #{response.status}." unless response.success? + + puts JSON.pretty_generate(JSON.parse(response.read)) + end end ~~~ -For more complex scenarios, including HTTP APIs, consider using [async-rest](https://github.com/socketry/async-rest) instead. +For resource-oriented HTTP APIs, consider using [`async-rest`](https://github.com/socketry/async-rest), which builds on `Async::HTTP`. -### Timeouts +## Applying a Timeout -To set a timeout for a request, use the `Task#with_timeout` method: +Networks can stall indefinitely, so impose a timeout around operations that must complete within a fixed duration: ~~~ ruby -require 'async/http/internet/instance' +require "async/http/internet/instance" Sync do |task| - # Request will timeout after 2 seconds task.with_timeout(2) do - response = Async::HTTP::Internet.get "https://httpbin.org/delay/10" - ensure - response&.close + Async::HTTP::Internet.get("https://httpbin.org/delay/10") do |response| + puts response.read + end end rescue Async::TimeoutError - puts "The request timed out" + warn "The request timed out." end ~~~ -### Making a Server +The response block still closes the response if the timeout interrupts the request while its body is being processed. + +## Making a Server -To create a server, use an instance of {ruby Async::HTTP::Server}: +{ruby Async::HTTP::Server} accepts an application that maps each request to a {ruby Protocol::HTTP::Response}. The following example starts a local server, makes one request, and then releases both client and server resources: ~~~ ruby -require 'async/http' +require "async/http" -endpoint = Async::HTTP::Endpoint.parse('http://localhost:9292') +endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292") +server = Async::HTTP::Server.for(endpoint) do |request| + Protocol::HTTP::Response[200, {"content-type" => "text/plain"}, ["Hello World"]] +end -Sync do |task| - Async(transient: true) do - server = Async::HTTP::Server.for(endpoint) do |request| - ::Protocol::HTTP::Response[200, {}, ["Hello World"]] - end - - server.run - end +Sync do + server_task = server.run - client = Async::HTTP::Client.new(endpoint) - response = client.get("/") - puts response.read + Async::HTTP::Client.open(endpoint) do |client| + response = client.get("/") + puts response.read + ensure + response&.close + end ensure - response&.close + server_task&.stop end ~~~ + +Use Falcon when you need to host a Rack application or deploy an HTTP server in production. Use `Async::HTTP::Server` directly when building a protocol-level server or embedding HTTP handling into another asynchronous application. diff --git a/guides/testing/readme.md b/guides/testing/readme.md index a9014543..e3d51f36 100644 --- a/guides/testing/readme.md +++ b/guides/testing/readme.md @@ -1,77 +1,177 @@ # Testing -This guide explains how to use `Async::HTTP` clients and servers in your tests. +This guide explains how to test `Async::HTTP` clients and servers without depending on external HTTP services. -In general, you should avoid making real HTTP requests in your tests. Instead, you should use a mock server or a fake client. +Real network services make tests slower and less deterministic. Prefer one of these approaches: -## Mocking HTTP Responses +- Use `sus-fixtures-async-http` to run an application with a managed local server and client. +- Use {ruby Async::HTTP::Mock::Endpoint} when testing a client that expects to connect to a particular remote endpoint. +- Use a small fake client when the HTTP protocol behavior itself is not under test. -The mocking feature of `Async::HTTP` uses a real server running in a separate task, and routes all requests to it. This allows you to intercept requests and return custom responses, but still use the real HTTP client. +## Testing an HTTP Application -In order to enable this feature, you must create an instance of {ruby Async::HTTP::Mock::Endpoint} which will handle the requests. +The `ServerContext` fixture manages an ephemeral listening endpoint, server task, and connected client. Add the fixture to your test dependencies: -~~~ ruby -require 'async/http' -require 'async/http/mock' +~~~ bash +$ bundle add sus --group test +$ bundle add sus-fixtures-async-http --group test +~~~ -mock_endpoint = Async::HTTP::Mock::Endpoint.new +Define the application under test and make requests through the provided `client`: -Sync do - # Start a background server: - server_task = Async(transient: true) do - mock_endpoint.run do |request| - # Respond to the request: - ::Protocol::HTTP::Response[200, {}, ["Hello, World"]] +~~~ ruby +require "sus/fixtures/async/http" + +describe "My HTTP application" do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:app) do + Protocol::HTTP::Middleware.for do |request| + case request.path + when "/health" + Protocol::HTTP::Response[ + 200, + {"content-type" => "application/json"}, + ['{"status":"ok"}'], + ] + else + Protocol::HTTP::Response[404, {}, ["Not Found"]] + end end end - endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") - mocked_endpoint = mock_endpoint.wrap(endpoint) - client = Async::HTTP::Client.new(mocked_endpoint) + it "serves the health endpoint" do + response = client.get("/health") + + expect(response).to be(:success?) + expect(response.headers["content-type"]).to be == "application/json" + expect(response.read).to be == '{"status":"ok"}' + ensure + response&.close + end - response = client.get("/") - puts response.read - # => "Hello, World" + it "returns not found for unknown paths" do + response = client.get("/missing") + expect(response.status).to be == 404 + ensure + response&.close + end end ~~~ -## Transparent Mocking +The fixture closes the client, stops the server, and releases the bound endpoint after each test. Override `app`, `url`, `protocol`, `endpoint_options`, or `retries` to configure a scenario. -Using your test framework's mocking capabilities, you can easily replace the `Async::HTTP::Client#new` with a method that returns a client with a mocked endpoint. +### Testing HTTP/2 -### Sus Integration +Override `protocol` when behavior must be verified with a specific HTTP version: ~~~ ruby -require 'async/http' -require 'async/http/mock' -require 'sus/fixtures/async/reactor_context' +describe "My HTTP/2 application" do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:protocol) {Async::HTTP::Protocol::HTTP2} + + it "responds using HTTP/2" do + response = client.get("/") + expect(response.version).to be == "HTTP/2" + ensure + response&.close + end +end +~~~ + +Test normal behavior without forcing a protocol unless the distinction is relevant to the feature under test. + +## Testing a Client with a Mock Endpoint -include Sus::Fixtures::Async::ReactorContext +{ruby Async::HTTP::Mock::Endpoint} connects the real client and server protocol implementations through a local socket pair. It does not open a network port, but requests still exercise serialization, connection handling, and response bodies. -let(:mock_endpoint) {Async::HTTP::Mock::Endpoint.new} +Use {ruby Async::HTTP::Mock::Endpoint#wrap} to preserve the scheme and authority expected by the client: -def before - super +~~~ ruby +require "async/http" +require "async/http/mock" +require "sus/fixtures/async/reactor_context" + +describe "A remote service client" do + include Sus::Fixtures::Async::ReactorContext - # Mock the HTTP client: - mock(Async::HTTP::Client) do |mock| - mock.wrap(:new) do |original, endpoint| - original.call(mock_endpoint.wrap(endpoint)) + it "handles a successful response" do + mock_endpoint = Async::HTTP::Mock::Endpoint.new + server_task = Async do + mock_endpoint.run do |request| + Protocol::HTTP::Response[200, {}, ["Authority: #{request.authority}"]] + end end + + remote_endpoint = Async::HTTP::Endpoint.parse("https://api.example.com") + client = Async::HTTP::Client.new(mock_endpoint.wrap(remote_endpoint)) + response = client.get("/status") + + expect(response.read).to be == "Authority: api.example.com" + ensure + response&.close + client&.close + server_task&.stop end +end +~~~ + +Return different statuses, headers, bodies, delays, or malformed behavior from the mock server to exercise client error handling. + +## Transparently Replacing Client Endpoints + +Some applications construct {ruby Async::HTTP::Client} internally. A test can wrap the constructor so those clients connect to a mock endpoint while retaining the original endpoint metadata and client options: + +~~~ ruby +require "async/http" +require "async/http/mock" +require "sus/fixtures/async/reactor_context" + +describe "A client created by application code" do + include Sus::Fixtures::Async::ReactorContext + + let(:mock_endpoint) {Async::HTTP::Mock::Endpoint.new} - # Run the mock server: - Async(transient: true) do - mock_endpoint.run do |request| - ::Protocol::HTTP::Response[200, {}, ["Hello, World"]] + def before + super + + replacement_endpoint = mock_endpoint + mock(Async::HTTP::Client) do |wrapper| + wrapper.wrap(:new) do |original, endpoint, **options| + original.call(replacement_endpoint.wrap(endpoint), **options) + end + end + + @server_task = Async do + mock_endpoint.run do |request| + Protocol::HTTP::Response[200, {}, ["Hello, World"]] + end end end -end - -it "should perform a web request" do - client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse("https://www.google.com")) - response = client.get("/") - # The response is mocked: - expect(response.read).to be == "Hello, World" + + def after(error = nil) + @server_task&.stop + super + end + + it "routes the request through the mock endpoint" do + endpoint = Async::HTTP::Endpoint.parse("https://api.example.com") + client = Async::HTTP::Client.new(endpoint, retries: 1) + response = client.get("/") + + expect(response.read).to be == "Hello, World" + ensure + response&.close + client&.close + end end ~~~ + +Always accept and forward `**options` when wrapping the constructor so the test does not silently change client configuration. + +## Choosing a Fake Client + +Use a fake client when application logic only needs a predetermined response and does not depend on HTTP framing, connection reuse, streaming, retries, or protocol errors. A fake is faster and simpler, but it cannot verify that requests are valid on the wire. + +Use a fixture or mock endpoint when the interaction with `Async::HTTP` is part of the behavior being tested. From a7551a81aaf2f3494cf4f25905c0b43e1ef0b100 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 21 Aug 2026 00:15:49 +1200 Subject: [PATCH 2/5] Use modern inline reference syntax Assisted-By: devx/eb0838cf-417f-4559-90ed-e9a0d84bfd2d --- guides/getting-started/readme.md | 14 +++++++------- guides/testing/readme.md | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 888d2101..7d4ddd7f 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -14,17 +14,17 @@ $ bundle add async-http `Async::HTTP` provides several interfaces for different kinds of HTTP applications: -- {ruby Async::HTTP::Internet} makes requests to arbitrary hosts and manages a client for each remote endpoint. -- {ruby Async::HTTP::Client} manages persistent connections to a specific endpoint. -- {ruby Async::HTTP::Server} accepts connections and dispatches requests to an HTTP application. -- {ruby Async::HTTP::Endpoint} describes how a client connects or a server listens, including the URL, protocol, and TLS configuration. +- ruby:`Async::HTTP::Internet` makes requests to arbitrary hosts and manages a client for each remote endpoint. +- ruby:`Async::HTTP::Client` manages persistent connections to a specific endpoint. +- ruby:`Async::HTTP::Server` accepts connections and dispatches requests to an HTTP application. +- ruby:`Async::HTTP::Endpoint` describes how a client connects or a server listens, including the URL, protocol, and TLS configuration. - [`protocol-http`](https://github.com/socketry/protocol-http) provides the shared request, response, header, and body interfaces. Use `Internet` for general-purpose requests to different hosts. Use `Client` when your application repeatedly communicates with one endpoint or needs endpoint-specific configuration. ## Making a Request -The shared {ruby Async::HTTP::Internet} instance provides a convenient starting point. Run asynchronous HTTP operations inside `Sync`, which creates or reuses the event loop while returning the block result directly: +The shared ruby:`Async::HTTP::Internet` instance provides a convenient starting point. Run asynchronous HTTP operations inside `Sync`, which creates or reuses the event loop while returning the block result directly: ~~~ ruby require "async/http/internet/instance" @@ -54,7 +54,7 @@ Convenience methods are provided for `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CO ### Connection Persistence -`Internet` creates a {ruby Async::HTTP::Client} for each remote endpoint and reuses its persistent connections. The underlying async pools are bound to the event loop and are closed when that event loop exits. +`Internet` creates a ruby:`Async::HTTP::Client` for each remote endpoint and reuses its persistent connections. The underlying async pools are bound to the event loop and are closed when that event loop exits. An explicitly created `Internet` can also be closed early when an application wants to release all cached clients before the event loop exits: @@ -156,7 +156,7 @@ The response block still closes the response if the timeout interrupts the reque ## Making a Server -{ruby Async::HTTP::Server} accepts an application that maps each request to a {ruby Protocol::HTTP::Response}. The following example starts a local server, makes one request, and then releases both client and server resources: +ruby:`Async::HTTP::Server` accepts an application that maps each request to a ruby:`Protocol::HTTP::Response`. The following example starts a local server, makes one request, and then releases both client and server resources: ~~~ ruby require "async/http" diff --git a/guides/testing/readme.md b/guides/testing/readme.md index e3d51f36..d2444793 100644 --- a/guides/testing/readme.md +++ b/guides/testing/readme.md @@ -5,7 +5,7 @@ This guide explains how to test `Async::HTTP` clients and servers without depend Real network services make tests slower and less deterministic. Prefer one of these approaches: - Use `sus-fixtures-async-http` to run an application with a managed local server and client. -- Use {ruby Async::HTTP::Mock::Endpoint} when testing a client that expects to connect to a particular remote endpoint. +- Use ruby:`Async::HTTP::Mock::Endpoint` when testing a client that expects to connect to a particular remote endpoint. - Use a small fake client when the HTTP protocol behavior itself is not under test. ## Testing an HTTP Application @@ -84,9 +84,9 @@ Test normal behavior without forcing a protocol unless the distinction is releva ## Testing a Client with a Mock Endpoint -{ruby Async::HTTP::Mock::Endpoint} connects the real client and server protocol implementations through a local socket pair. It does not open a network port, but requests still exercise serialization, connection handling, and response bodies. +ruby:`Async::HTTP::Mock::Endpoint` connects the real client and server protocol implementations through a local socket pair. It does not open a network port, but requests still exercise serialization, connection handling, and response bodies. -Use {ruby Async::HTTP::Mock::Endpoint#wrap} to preserve the scheme and authority expected by the client: +Use ruby:`Async::HTTP::Mock::Endpoint#wrap` to preserve the scheme and authority expected by the client: ~~~ ruby require "async/http" @@ -121,7 +121,7 @@ Return different statuses, headers, bodies, delays, or malformed behavior from t ## Transparently Replacing Client Endpoints -Some applications construct {ruby Async::HTTP::Client} internally. A test can wrap the constructor so those clients connect to a mock endpoint while retaining the original endpoint metadata and client options: +Some applications construct ruby:`Async::HTTP::Client` internally. A test can wrap the constructor so those clients connect to a mock endpoint while retaining the original endpoint metadata and client options: ~~~ ruby require "async/http" From 078b794ebd7e8b1107ddd930b565b5c38e4cc743 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 21 Aug 2026 00:24:15 +1200 Subject: [PATCH 3/5] Remove fake client guidance Assisted-By: devx/eb0838cf-417f-4559-90ed-e9a0d84bfd2d --- guides/testing/readme.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/guides/testing/readme.md b/guides/testing/readme.md index d2444793..2cf2f10e 100644 --- a/guides/testing/readme.md +++ b/guides/testing/readme.md @@ -169,9 +169,3 @@ end ~~~ Always accept and forward `**options` when wrapping the constructor so the test does not silently change client configuration. - -## Choosing a Fake Client - -Use a fake client when application logic only needs a predetermined response and does not depend on HTTP framing, connection reuse, streaming, retries, or protocol errors. A fake is faster and simpler, but it cannot verify that requests are valid on the wire. - -Use a fixture or mock endpoint when the interaction with `Async::HTTP` is part of the behavior being tested. From 8ad6ab650b8ceeebf1c92a57962a3742d05bf274 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 21 Aug 2026 00:27:48 +1200 Subject: [PATCH 4/5] Link protocol-http body documentation Assisted-By: devx/eb0838cf-417f-4559-90ed-e9a0d84bfd2d --- guides/getting-started/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 7d4ddd7f..2dbe1292 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -91,7 +91,7 @@ Sync do end ~~~ -For larger responses, process the body incrementally rather than reading it into one string. See the `protocol-http` message body documentation for the complete body interface. +For larger responses, process the body incrementally rather than reading it into one string. See the [`protocol-http` message body documentation](https://socketry.github.io/protocol-http/guides/message-body/) for the complete body interface. ### Downloading a File From ea78b238ea67c876ff33fa0d8518d564988d04a4 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 21 Aug 2026 00:29:45 +1200 Subject: [PATCH 5/5] Document in-process middleware testing Assisted-By: devx/eb0838cf-417f-4559-90ed-e9a0d84bfd2d --- guides/testing/readme.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/guides/testing/readme.md b/guides/testing/readme.md index 2cf2f10e..7b01a002 100644 --- a/guides/testing/readme.md +++ b/guides/testing/readme.md @@ -4,11 +4,45 @@ This guide explains how to test `Async::HTTP` clients and servers without depend Real network services make tests slower and less deterministic. Prefer one of these approaches: +- Use [`sus-fixtures-protocol-http`](https://socketry.github.io/sus-fixtures-protocol-http/guides/getting-started/) to exercise HTTP middleware directly without a client or server. - Use `sus-fixtures-async-http` to run an application with a managed local server and client. - Use ruby:`Async::HTTP::Mock::Endpoint` when testing a client that expects to connect to a particular remote endpoint. -- Use a small fake client when the HTTP protocol behavior itself is not under test. -## Testing an HTTP Application +## Testing Middleware Directly + +When a test only needs to construct requests and inspect responses, `sus-fixtures-protocol-http` can call `Protocol::HTTP` middleware in-process without starting a client or server. Add the fixture to your test dependencies: + +~~~ bash +$ bundle add sus --group test +$ bundle add sus-fixtures-protocol-http --group test +~~~ + +Include `MiddlewareContext` and provide the middleware under test: + +~~~ ruby +require "sus/fixtures/protocol/http/middleware_context" + +describe "My HTTP application" do + include Sus::Fixtures::Protocol::HTTP::MiddlewareContext + + let(:middleware) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, {}, ["Hello #{request.path}"]] + end + end + + it "handles a request directly" do + response = client.get("/world") + + expect(response.status).to be == 200 + expect(response.read).to be == "Hello /world" + end +end +~~~ + +The fixture closes the final request, response, and middleware after each test. Use `sus-fixtures-async-http` when the test needs a real client/server exchange. + +## Testing with a Client and Server The `ServerContext` fixture manages an ephemeral listening endpoint, server task, and connected client. Add the fixture to your test dependencies: