httpserver

package
v1.2.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

httpserver

Serves net/http handlers on TinyGo when one of them needs to take over the connection.

import "github.com/shibukawa/tinygodriver/httpserver"

Why this exists

TinyGo's net/http server cannot complete a protocol upgrade. Before calling a handler it starts a background read on the connection, and it cancels that read by moving the read deadline into the past. netdev takes a deadline by value when a read begins, so it cannot interrupt a recv() already in flight: the cancellation never lands and Hijack blocks forever.

A WebSocket handshake served this way hangs with no error, no panic and no log line. The client times out; the server logs nothing.

This package reads the request head itself and decides where each connection goes. Anything that is not an upgrade is handed to a real http.Server, with the head replayed, so keep-alive, timeouts and graceful shutdown keep working. An upgrade reaches the handler through a ResponseWriter that implements http.Hijacker, with no background read in the way.

Under standard Go, Serve calls srv.Serve(ln) and nothing else. net/http can hijack there, so none of this is needed and none of it runs.

Usage

One listener, one port. A WebSocket endpoint is one route among many.

mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthz)
mux.HandleFunc("/ws", serveWebSocket) // calls websocket.Upgrader.Upgrade

ln, err := net.Listen("tcp", ":8080")
if err != nil {
	return err
}
return httpserver.Serve(ln, &http.Server{Handler: mux})

srv.Shutdown and srv.Close still work: the package serves the caller's own http.Server rather than a copy, so the connections it hands over stay under that server's control.

Configuration

ServeConfig takes a Config:

Field Meaning
ShouldBypass Which requests need a hijackable connection. nil means IsUpgrade, which matches the Connection: upgrade token and so covers WebSocket without naming it.
ReadHeaderTimeout Bounds the read of the request head. Zero takes http.Server.ReadHeaderTimeout, then DefaultReadHeaderTimeout (10s). Negative means no limit.

net/http defaults to no header timeout; this package does not, because reading the head is its own job here and an unbounded read is a goroutine a stalled client can hold forever.

Keep ShouldBypass narrow. A request it accepts reaches a ResponseWriter that implements Header, Write, WriteHeader and Hijack and nothing else — no Flush, no ReadFrom, no CloseNotify, no trailers, no chunked encoding. That writer exists to be hijacked. A handler that needs the rest is not an upgrade handler and should go to http.Server.

Limits

Only the first request on a connection is inspected. A browser opens a fresh connection for a WebSocket handshake, so this holds in practice. An upgrade arriving as a later request on a reused connection is answered 501 Not Implemented rather than deadlocking. Inspecting every request would mean reimplementing http.Server, which this package deliberately does not do.

An upgrade request must carry no body and no early data. A client that sends some is answered 400: the hijacked reader starts at the connection, not at those leftover bytes, so accepting them would drop them silently. RFC 6455 forbids a body in the handshake, so this should never fire for WebSocket.

Plaintext only. TinyGo's http.Server has no ServeTLS and its crypto/tls no Server or X509KeyPair. Terminate TLS in front of the process.

When this package stops being necessary

It works around one upstream defect and nothing else. It becomes unnecessary the moment TinyGo's net makes deadlines live — by re-checking a mutable deadline, or by polling a non-blocking socket — or TinyGo's net/http stops starting the background read. Either fix makes plain http.Server work, and callers can drop the Serve call for srv.Serve.

The defect is not fixable in netdev: the Netdever interface passes the deadline by value per call, so no driver change can interrupt a call already in flight. It is the same root cause that makes SetDeadline-based query cancellation ineffective for the PostgreSQL driver.

Implementation selection

Follows the repository convention: server_std.go carries !tinygo && !force_tinygo_logic, server_tinygo.go and bypasswriter_tinygo.go carry tinygo || force_tinygo_logic. Building with -tags force_tinygo_logic runs the TinyGo path under host Go, which is how most of the test suite is exercised without a TinyGo toolchain.

Backend reports which path was selected, "std" or "tinygo".

Tests

go test ./httpserver                            # std path
go test -tags force_tinygo_logic ./httpserver   # TinyGo path, host toolchain
tinygo test ./httpserver                        # TinyGo path, real compiler

Two TinyGo facts shape the tests, and any test added here: net.Listener.Addr() reports port 0 for a port 0 listen under netdev, so the port must be chosen by the test; and t.Fatalf does not stop the goroutine, so every failure needs an explicit return.

Documentation

Overview

Package httpserver serves net/http handlers on TinyGo when one of them needs to take over the connection.

TinyGo's net/http Server cannot complete a protocol upgrade. Before calling a handler it starts a background read on the connection, and it cancels that read by moving the read deadline into the past. The netdev driver takes a deadline by value when a read begins, so it cannot interrupt a recv() already in flight: the cancellation never lands, and Hijack blocks forever. A WebSocket handshake hangs with no error, no panic and no log line.

This package reads the request head itself and decides where the connection goes. Anything that is not an upgrade is handed to a real http.Server, with the head replayed, so keep-alive, timeouts and graceful shutdown keep working. An upgrade reaches the handler through a ResponseWriter that implements http.Hijacker, without net/http's background read in the way.

One listener, one port. A WebSocket endpoint is one route among many:

mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthz)
mux.HandleFunc("/ws", serveWebSocket) // calls Upgrader.Upgrade

ln, err := net.Listen("tcp", ":8080")
if err != nil {
	return err
}
httpserver.Serve(ln, &http.Server{Handler: mux})

Under standard Go, Serve calls srv.Serve(ln) and nothing else: net/http can hijack there, so none of this is needed. The same source builds and behaves the same way under both compilers.

When this package stops being necessary

The whole package works around one upstream defect. It becomes unnecessary the moment TinyGo's net makes deadlines live, by re-checking a mutable deadline or polling a non-blocking socket, or TinyGo's net/http stops starting the background read. Either fix makes plain http.Server work, and callers can drop the Serve call for srv.Serve.

Known limits

Only the first request on a connection is inspected. A browser opens a fresh connection for a WebSocket handshake, so this holds in practice; an upgrade arriving as a later request on a reused connection is answered 501 rather than deadlocking. Inspecting every request would mean reimplementing http.Server, which this package deliberately does not do.

The bypass ResponseWriter exists to be hijacked. It implements Header, Write, WriteHeader and Hijack. It does not implement Flush, ReadFrom, CloseNotify, trailers or chunked encoding: a handler that needs those is not an upgrade handler and should not be reached through the bypass predicate.

Index

Constants

View Source
const Backend = "std"

Backend identifies the implementation selected by build constraints.

View Source
const DefaultReadHeaderTimeout = 10 * time.Second

DefaultReadHeaderTimeout bounds the request head when neither Config nor the http.Server sets a limit. net/http defaults to no limit; this package does not, because reading the head is its own job here and an unbounded read is a goroutine a stalled client can hold forever.

Variables

View Source
var ErrNilServer = errors.New("httpserver: nil *http.Server")

ErrNilServer reports a Serve call with no server to serve.

Functions

func IsUpgrade

func IsUpgrade(r *http.Request) bool

IsUpgrade reports whether r asks to switch protocols, which is the default bypass predicate. It matches the Connection token rather than a specific protocol, so it covers WebSocket without this package knowing what that is.

func ListenAndServe

func ListenAndServe(addr string, h http.Handler) error

ListenAndServe listens on addr and serves h.

func Serve

func Serve(ln net.Listener, srv *http.Server) error

Serve accepts connections on ln and serves them through srv, letting a handler hijack. It is srv.Serve(ln) plus whatever the platform needs.

Serve returns whatever ended the accept loop, matching http.Server.Serve, which returns ErrServerClosed after Shutdown or Close.

func ServeConfig

func ServeConfig(ln net.Listener, srv *http.Server, cfg Config) error

ServeConfig is Serve with the serving path configured.

On the TinyGo path ServeConfig replaces srv.Handler with a wrapper before serving. It does this rather than serving a copy so that srv.Shutdown and srv.Close still govern the connections, and it happens once, before the listener is read, so no request observes the change.

Types

type Config

type Config struct {
	// ShouldBypass reports whether a request must reach the handler over a
	// connection it can hijack. nil means IsUpgrade.
	//
	// Returning true for a request whose handler does not hijack still works,
	// but that handler gives up everything http.Server provides, so keep the
	// predicate narrow.
	ShouldBypass func(*http.Request) bool

	// ReadHeaderTimeout bounds the read of the request head. Zero takes
	// http.Server.ReadHeaderTimeout, and if that is zero too,
	// DefaultReadHeaderTimeout. A negative value means no limit.
	ReadHeaderTimeout time.Duration
	// contains filtered or unexported fields
}

Config tunes the TinyGo serving path. The zero value is valid and is what Serve uses.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL