net/http Driver
The standard-library HTTP driver adapts generated HTTP and WebSocket routes to
net/http.
Install it:
go get github.com/TDB-Group/anvil-http/stdImport it as:
import httpstd "github.com/TDB-Group/anvil-http/std"Basic Use
Section titled “Basic Use”app := anvil.New( httpstd.Driver(),)When installed through anvil.New, the driver registers its HTTP and WebSocket
capabilities with Anvil. Anvil’s edge listener accepts requests and dispatches
HTTP fallback traffic to this driver.
The same driver can also run standalone through httpstd.New, but normal Anvil
applications let Anvil own the public listener.
Configuration
Section titled “Configuration”The driver accepts zero options or one httpstd.Options value:
app := anvil.New( httpstd.Driver(httpstd.Options{ MaxBodyBytes: 8 << 20, }),)Supported options:
| Option | Behavior |
|---|---|
Server | Cloned *http.Server settings for standalone driver use. Handler must be nil because the driver installs it. In a normal edge app, public listener settings come from anvil.WithListener. |
Codecs | Request decoder and response encoder registry. Defaults to Anvil’s default codec registry. |
ErrorPipeline | Error mapper and observer pipeline. Driver fills this from app.ErrorPipeline() when the option is nil. |
TLS | Cert/key pair used when the driver is run standalone. In a normal edge app, use app.ListenTLS. |
WebSocket | Origin patterns and insecure origin bypass for WebSocket upgrades. |
MaxBodyBytes | Maximum request body size. 0 selects the driver default of 4 MiB; negative values are rejected. Set a larger value for upload endpoints. |
The driver rejects negative timeouts, negative body limits, incomplete TLS
configuration, nil codec registries, a server with a pre-filled Handler, and
more than one options value.
DriverFrom installs an existing transport and sets that transport’s error
pipeline to the owning app pipeline. Use
httpstd.New(httpstd.Options{ErrorPipeline: ...}) when the transport is
running standalone or when a test needs to inspect the transport before it is
installed.
MaxBodyBytes wraps the request body with http.MaxBytesReader when the
configured value is greater than zero. The option cannot disable the default
limit by setting 0; 0 means “use the default.” The limit is observed when
handler code or generated binding reads the body. Decode maps a limit error
to 413 Request Entity Too Large; Body() returns nil if the body cannot be
read because the SDK body accessor has no error return.
Server and TLS are only used when the standard driver runs its own
standalone server. In the normal Anvil runtime, edge serves the public listener
and calls the driver as an http.Handler. Codec settings, body limits,
WebSocket origin policy, route matching, and response behavior still belong to
the driver.
Use anvil.WithListener for public listener settings in an edge app:
app := anvil.New( anvil.WithListener(&http.Server{ ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, }), httpstd.Driver(),)When a custom Server leaves timeout fields empty, the driver applies these
defaults to the cloned server:
| Server setting | Default |
|---|---|
ReadHeaderTimeout | 5s |
ReadTimeout | 30s |
WriteTimeout | 30s |
IdleTimeout | 2m |
Route Matching
Section titled “Route Matching”Generated routes are registered with RegisterHTTP.
Generated code performs route-collision checks before the driver sees a route. The standard driver validates the registered route shape, then optimizes the runtime match path. It is not the compiler and it does not try to explain conflicting source declarations.
Static routes are stored in a map keyed by method and normalized path. Parameter
routes use segment matching and only allocate parameter storage when the route
actually contains :params.
If code manually registers duplicate routes against the transport, the driver does not produce compiler-style collision diagnostics. A duplicate static route with the same method and normalized path replaces the previous static route in the map. Duplicate parameter routes are appended, and the first matching route in registration order handles the request. Generated Anvil code is checked for collisions before emission, so duplicate generated routes fail at compile time.
For matching, the standard driver trims outer slashes from registered patterns
and request paths. /api/projects and /api/projects/ match the same route.
It does not clean or collapse interior path segments; write the route path you
intend to serve.
Only the URL path participates in route matching. Query strings are available
through ctx.Request().Query(name) after a route has matched.
Route params use Go web router style:
type Routes struct { Get sdk.GET `path:"/:projectId"`}{projectId} is not Anvil route syntax.
Response Behavior
Section titled “Response Behavior”Handlers return the body as the normal Go return value. Response metadata lives
on ctx.Response():
ctx.Response().Status(http.StatusCreated)ctx.Response().Header("Location", "/projects/"+created.ID.String())return projectDTO(created), nilIf the handler returns nil and no status was set, the driver sends
204 No Content. If a body is returned and no status was set, the driver sends
200 OK.
Returned bodies are encoded through the codec registry using the request
Accept header.
Invalid response metadata is treated as a handler error. For example, an empty
header name or a status code outside 100..999 is sent through the same error
pipeline as a returned error.
Mapped HTTP failures are encoded through the same codec registry and request
Accept header as normal responses. If that error response cannot be encoded,
the driver falls back to HTTP 500 with a fixed JSON internal-error body.
Streaming
Section titled “Streaming”ctx.Response().Stream writes through http.ResponseWriter and flushes when
the writer supports http.Flusher.
The stream handler cannot return a separate response body. If both a stream and body are set, the driver maps that as an error.
The driver writes the response status when the stream first writes or flushes.
If the stream never writes, the driver still commits the configured status, or
200 OK when no status was set. A nil stream handler is rejected immediately.
Calling Stream a second time returns an error immediately and keeps the first
stream handler.
Portable handlers set stream status and headers through ctx.Response() before
the stream starts. The SDK stream contract itself only exposes Context,
Write, and Flush.
Stream errors follow normal Anvil error mapping, but HTTP response commitment
still follows net/http rules. If the stream returns an error before the first
write or flush, the driver can still write a mapped error response. If the
stream has already written or flushed, the status and headers are committed.
The driver still maps and publishes the failure, but the client may receive a
partial stream instead of a clean JSON error body.
WebSockets
Section titled “WebSockets”sdk.WS routes use the driver’s WebSocket integration. HTTP middleware runs
before the upgrade, which is where auth and rate limits belong.
Origin policy is controlled by Options.WebSocket.
The standard driver only checks WebSocket routes when the request contains
WebSocket upgrade headers. The socket library validates the handshake during
accept. A normal HTTP request to a WebSocket-only path is not a match and
returns 404.
Before the upgrade, handler and middleware errors are mapped into normal HTTP responses. After the upgrade succeeds, the driver maps the failure, publishes the error event, and closes the socket; there is no HTTP response left to write.
The driver uses github.com/coder/websocket. socket.Native() returns
*websocket.Conn. The portable socket.Write method supports text and binary
messages; use socket.Close for close frames.
Reads from the portable socket expose text and binary messages. Other frame
types are driver-level behavior unless the handler deliberately uses
socket.Native().
Native Context
Section titled “Native Context”ctx.Native() returns:
httpstd.NativeContext{ ResponseWriter: writer, Request: request,}Using it ties the handler to this driver. Keep it for deliberate escape hatches, not normal application flow.
Standalone Use
Section titled “Standalone Use”httpstd.New creates the transport directly. DriverFrom installs an existing
transport into an app:
transport := httpstd.New()app := anvil.New(httpstd.DriverFrom(transport))This is useful for tests and low-level integration work.
Standalone mode can call transport.Start(addr) or transport.ServeConn(conn).
Edge mode uses the transport as an http.Handler, so public listening and TLS
termination stay in Anvil.
Standalone behavior is intentionally thin:
Start(addr)sets the cloned server address and callsListenAndServeorListenAndServeTLS.ServeConn(conn)serves one accepted connection and normalizeshttp.ErrServerClosedto nil.Shutdown(ctx)calls the cloned server’sShutdownand normalizeshttp.ErrServerClosedto nil.ServeHTTPis the path used by Anvil edge after request classification.
Testbed Runner
Section titled “Testbed Runner”httpstd.RunTestbed(ctx, suite, opts) executes HTTP, GraphQL-over-HTTP, and
WebSocket cases against a running server. Anvil defines the protocol-neutral
testbed model, but the standard driver executes WebSocket cases because it
imports the socket client.
The WebSocket runner converts http URLs to ws and https URLs to wss.
It supports scripted text, binary, ping, and close input messages. It asserts
text and binary output messages, then checks the optional close code. It does
not assert ping or pong output frames.
Each WebSocket case runs with a 10s timeout inside the driver runner. An
expected close code of 0 skips close-status assertion. Any non-zero close
code makes the runner read one more frame and compare the WebSocket close
status.
Use it when a generated suite contains WebSocket cases:
report, err := httpstd.RunTestbed(ctx, suite, testbed.RunOptions{ BaseURL: "http://127.0.0.1:8080",})