Skip to content

Commit 7a1fd3d

Browse files
authored
add support for http trailers (#232)
* fsthttp,internal: add support for http trailers on http responses * fsthttp,internal: add support for Trailers * fsthttp: trailers docs++
1 parent c19df65 commit 7a1fd3d

3 files changed

Lines changed: 299 additions & 4 deletions

File tree

fsthttp/response.go

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"net"
10+
"net/textproto"
1011
"strconv"
1112
"strings"
1213

@@ -33,6 +34,8 @@ type Response struct {
3334
// Body of the response.
3435
Body io.ReadCloser
3536

37+
trailers Header
38+
3639
cacheResponse cacheResponse
3740

3841
abi struct {
@@ -71,6 +74,45 @@ func (resp *Response) RemoteAddr() (net.Addr, error) {
7174
return &addr, nil
7275
}
7376

77+
var ErrTrailersNotReady = errors.New("trailers not available")
78+
79+
// Trailers returns the trailers associated with the response. Can only be called after the response Body returns EOF.
80+
func (resp *Response) Trailers() (Header, error) {
81+
if resp.trailers != nil {
82+
return resp.trailers, nil
83+
}
84+
85+
// This might happen if the Body field is replaced before Trailers is called.
86+
abiBody, ok := resp.Body.(*fastly.HTTPBody)
87+
if !ok {
88+
return nil, fmt.Errorf("Response.Body is not an HTTP Response Body")
89+
}
90+
91+
trailers := NewHeader()
92+
93+
keys := abiBody.GetTrailerNames()
94+
for keys.Next() {
95+
k := string(keys.Bytes())
96+
vals := abiBody.GetTrailerValues(k)
97+
for vals.Next() {
98+
v := string(vals.Bytes())
99+
trailers.Add(k, v)
100+
}
101+
if err := vals.Err(); err != nil {
102+
return nil, fmt.Errorf("read trailer key %q: %w", k, err)
103+
}
104+
}
105+
if err := keys.Err(); err != nil {
106+
if e, ok := fastly.IsFastlyError(err); ok && e == fastly.FastlyStatusAgain {
107+
return nil, ErrTrailersNotReady
108+
}
109+
return nil, fmt.Errorf("read trailer keys: %w", err)
110+
}
111+
112+
resp.trailers = trailers
113+
return resp.trailers, nil
114+
}
115+
74116
type netaddr struct {
75117
ip net.IP
76118
port uint16
@@ -210,7 +252,18 @@ func (resp *Response) SurrogateKeys() string {
210252
// ResponseWriter is used to respond to client requests.
211253
type ResponseWriter interface {
212254
// Header returns the headers that will be sent by WriteHeader.
213-
// Changing the returned headers after a call to WriteHeader has no effect.
255+
// The [Header] map also is the mechanism with which implementations can set HTTP trailers.
256+
// Changing the returned headers after a call to WriteHeader has no effect unless the modified headers are Trailers.
257+
//
258+
// There are two ways to set Trailers. The preferred way is to
259+
// predeclare in the headers which trailers you will later
260+
// send by setting the "Trailer" header to the names of the
261+
// trailer keys which will come later. In this case, those
262+
// keys of the Header map are treated as if they were
263+
// trailers. See the example. The second way, for trailer
264+
// keys not known to the [Handler] until after the first [ResponseWriter.Write],
265+
// is to prefix the [Header] map keys with the [TrailerPrefix]
266+
// constant value.
214267
Header() Header
215268

216269
// WriteHeader initiates the response to the client by sending an HTTP
@@ -267,6 +320,7 @@ type responseWriter struct {
267320
closed bool
268321
ManualFramingMode bool
269322
sendErr error
323+
trailers []string
270324
}
271325

272326
func newResponseWriter() (*responseWriter, error) {
@@ -291,7 +345,7 @@ func (resp *responseWriter) Header() Header {
291345
return resp.header
292346
}
293347

294-
var excludeHeadersNoBody = map[string]bool{CanonicalHeaderKey("Content-Length"): true, CanonicalHeaderKey("Transfer-Encoding"): true}
348+
var excludeHeadersNoBody = map[string]bool{CanonicalHeaderKey("Content-Length"): true, CanonicalHeaderKey("Transfer-Encoding"): true, CanonicalHeaderKey("Trailer"): true}
295349

296350
var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ")
297351

@@ -337,6 +391,9 @@ func (resp *responseWriter) WriteHeader(code int) {
337391
}
338392
}
339393

394+
// Store list of trailers for later.
395+
resp.trailers = parseTrailers(resp.header.Values("Trailer"))
396+
340397
if code == StatusEarlyHints {
341398
// For early hints, don't mark the headers as "sent" so we can send them again next time.
342399
return
@@ -345,6 +402,18 @@ func (resp *responseWriter) WriteHeader(code int) {
345402
resp.wroteHeaders = true
346403
}
347404

405+
func parseTrailers(trailers []string) []string {
406+
var result []string
407+
for _, v := range trailers {
408+
for _, s := range strings.Split(v, ",") {
409+
if h := textproto.TrimString(s); h != "" {
410+
result = append(result, h)
411+
}
412+
}
413+
}
414+
return result
415+
}
416+
348417
var (
349418
// ErrClosed is returned when attempting to write to a ResponseWriter whose network connection has been closed.
350419
ErrClosed = errors.New("connection has been closed")
@@ -378,9 +447,53 @@ func (resp *responseWriter) Close() error {
378447
return nil
379448
}
380449
resp.closed = true
450+
451+
if err := resp.writeTrailers(); err != nil {
452+
return err
453+
}
454+
381455
return resp.abiBody.Close()
382456
}
383457

458+
// TrailerPrefix is a magic prefix for [ResponseWriter.Header] map keys
459+
// that, if present, signals that the map entry is actually for the response
460+
// trailers, and not the response headers. The prefix is stripped after the
461+
// ServeHTTP call finishes and the values are sent in the trailers.
462+
//
463+
// This mechanism is intended only for trailers that are not known prior to the
464+
// headers being written. If the set of trailers is fixed or known before
465+
// the header is written, the normal Go trailers mechanism is preferred.
466+
const TrailerPrefix = "Trailer:"
467+
468+
func (resp *responseWriter) writeTrailers() error {
469+
t := NewHeader()
470+
471+
// Get trailers from header map with TrailerPrefix
472+
for _, h := range resp.header.Keys() {
473+
if key, ok := strings.CutPrefix(h, TrailerPrefix); ok {
474+
if v := resp.header.Values(h); len(v) > 0 {
475+
t[key] = v
476+
}
477+
}
478+
}
479+
480+
// Extract Trailer names from saved list.
481+
for _, h := range resp.trailers {
482+
if v := resp.header.Values(h); len(v) > 0 {
483+
t[h] = v
484+
}
485+
}
486+
487+
// Write out the trailers.
488+
for k, v := range t {
489+
if err := resp.abiBody.TrailerAppend(k, v[0]); err != nil {
490+
println("error during trailer append: ", err.Error())
491+
}
492+
}
493+
494+
return nil
495+
}
496+
384497
func (resp *responseWriter) SetManualFramingMode(mode bool) {
385498
resp.ManualFramingMode = mode
386499
}
@@ -396,6 +509,5 @@ func (resp *responseWriter) Append(other io.ReadCloser) error {
396509
if !ok {
397510
return fmt.Errorf("non-Response Body passed to ResponseWriter.Append")
398511
}
399-
resp.abiBody.Append(otherAbiBody)
400-
return nil
512+
return resp.abiBody.Append(otherAbiBody)
401513
}

internal/abi/fastly/hostcalls_noguest.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ func (b *HTTPBody) Length() (uint64, error) {
4343
return 0, fmt.Errorf("not implemented")
4444
}
4545

46+
func (b *HTTPBody) TrailerAppend(name, value string) error {
47+
return fmt.Errorf("not implemented")
48+
}
49+
50+
func (b *HTTPBody) GetTrailerNames() *Values {
51+
return nil
52+
}
53+
54+
func (b *HTTPBody) GetTrailerValue(name string, maxHeaderValueLen int) (string, error) {
55+
return "", fmt.Errorf("not implemented")
56+
}
57+
58+
func (b *HTTPBody) GetTrailerValues(name string) *Values {
59+
return nil
60+
}
61+
4662
type LogEndpoint struct{}
4763

4864
func GetLogEndpoint(name string) (*LogEndpoint, error) {

internal/abi/fastly/http_guest.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,173 @@ func (b *HTTPBody) Length() (uint64, error) {
21282128
return uint64(l), nil
21292129
}
21302130

2131+
// witx:
2132+
//
2133+
// (@interface func (export "trailer_append")
2134+
// (param $h $body_handle)
2135+
// (param $name (list u8))
2136+
// (param $value (list u8))
2137+
// (result $err (expected (error $fastly_status)))
2138+
// )
2139+
//
2140+
//go:wasmimport fastly_http_body trailer_append
2141+
//go:noescape
2142+
func fastlyHTTPBodyTrailerAppend(
2143+
h bodyHandle,
2144+
nameData prim.Pointer[prim.U8], nameLen prim.Usize,
2145+
valuesData prim.Pointer[prim.U8], valuesLen prim.Usize, // multiple values separated by \0
2146+
) FastlyStatus
2147+
2148+
// TrailerAppend appends a name/value pair as an HTTP Trailer
2149+
func (r *HTTPBody) TrailerAppend(name string, value string) error {
2150+
nameBuffer := prim.NewReadBufferFromString(name).ArrayU8()
2151+
valueBuffer := prim.NewReadBufferFromString(value).ArrayU8()
2152+
2153+
return fastlyHTTPBodyTrailerAppend(
2154+
r.h,
2155+
nameBuffer.Data, nameBuffer.Len,
2156+
valueBuffer.Data, valueBuffer.Len,
2157+
).toError()
2158+
}
2159+
2160+
// witx:
2161+
//
2162+
// (@interface func (export "trailer_names_get")
2163+
// (param $h $body_handle)
2164+
// (param $buf (@witx pointer (@witx char8)))
2165+
// (param $buf_len (@witx usize))
2166+
// (param $cursor $multi_value_cursor)
2167+
// (param $ending_cursor_out (@witx pointer $multi_value_cursor_result))
2168+
// (param $nwritten_out (@witx pointer (@witx usize)))
2169+
// (result $err (expected (error $fastly_status)))
2170+
// )
2171+
//
2172+
//go:wasmimport fastly_http_body trailer_names_get
2173+
//go:noescape
2174+
func fastlyHTTPBodyTrailerNamesGet(
2175+
h bodyHandle,
2176+
buf prim.Pointer[prim.Char8],
2177+
bufLen prim.Usize,
2178+
cursor multiValueCursor,
2179+
endingCursorOut prim.Pointer[multiValueCursorResult],
2180+
nwrittenOut prim.Pointer[prim.Usize],
2181+
) FastlyStatus
2182+
2183+
// GetTrailerNames returns an iterator that yields the names of each trailer of
2184+
// the request.
2185+
func (r *HTTPBody) GetTrailerNames() *Values {
2186+
adapter := func(
2187+
buf *prim.Char8,
2188+
bufLen prim.Usize,
2189+
cursor multiValueCursor,
2190+
endingCursorOut *multiValueCursorResult,
2191+
nwrittenOut *prim.Usize,
2192+
) FastlyStatus {
2193+
return fastlyHTTPBodyTrailerNamesGet(
2194+
r.h,
2195+
prim.ToPointer(buf), bufLen,
2196+
cursor,
2197+
prim.ToPointer(endingCursorOut),
2198+
prim.ToPointer(nwrittenOut),
2199+
)
2200+
}
2201+
2202+
return newValues(adapter, DefaultMediumBufLen) // Large enough to get most header names in a single call.
2203+
}
2204+
2205+
// witx:
2206+
//
2207+
// (@interface func (export "trailer_value_get")
2208+
// (param $h $body_handle)
2209+
// (param $name (list u8))
2210+
// (param $value (@witx pointer (@witx char8)))
2211+
// (param $value_max_len (@witx usize))
2212+
// (param $nwritten_out (@witx pointer (@witx usize)))
2213+
// (result $err (expected (error $fastly_status)))
2214+
// )
2215+
//
2216+
//go:wasmimport fastly_http_body trailer_value_get
2217+
//go:noescape
2218+
func fastlyHTTPBodyTrailerValueGet(
2219+
h bodyHandle,
2220+
nameData prim.Pointer[prim.U8], nameLen prim.Usize,
2221+
value prim.Pointer[prim.Char8],
2222+
valueMaxLen prim.Usize,
2223+
nwrittenOut prim.Pointer[prim.Usize],
2224+
) FastlyStatus
2225+
2226+
// GetTrailerValue returns the first trailer value of the given trailer name on the
2227+
// request, if any.
2228+
func (r *HTTPBody) GetTrailerValue(name string) (string, error) {
2229+
// Most header keys are short: e.g. "Host", "Content-Type", "User-Agent", etc.
2230+
nameBuffer := prim.NewReadBufferFromString(name).ArrayU8()
2231+
value, err := withAdaptiveBuffer(DefaultSmallBufLen, func(buf *prim.WriteBuffer) FastlyStatus {
2232+
return fastlyHTTPBodyTrailerValueGet(
2233+
r.h,
2234+
nameBuffer.Data, nameBuffer.Len,
2235+
prim.ToPointer(buf.Char8Pointer()),
2236+
buf.Cap(),
2237+
prim.ToPointer(buf.NPointer()),
2238+
)
2239+
})
2240+
if err != nil {
2241+
return "", err
2242+
}
2243+
return value.ToString(), nil
2244+
}
2245+
2246+
// witx:
2247+
//
2248+
// (@interface func (export "trailer_values_get")
2249+
//
2250+
// (param $h $body_handle)
2251+
// (param $name (list u8))
2252+
// (param $buf (@witx pointer (@witx char8)))
2253+
// (param $buf_len (@witx usize))
2254+
// (param $cursor $multi_value_cursor)
2255+
// (param $ending_cursor_out (@witx pointer $multi_value_cursor_result))
2256+
// (param $nwritten_out (@witx pointer (@witx usize)))
2257+
// (result $err (expected (error $fastly_status)))
2258+
//
2259+
// )
2260+
//
2261+
//go:wasmimport fastly_http_body trailer_values_get
2262+
//go:noescape
2263+
func fastlyHTTPBodyTrailerValuesGet(
2264+
h bodyHandle,
2265+
nameData prim.Pointer[prim.U8], nameLen prim.Usize,
2266+
buf prim.Pointer[prim.Char8],
2267+
bufLen prim.Usize,
2268+
cursor multiValueCursor,
2269+
endingCursorOut prim.Pointer[multiValueCursorResult],
2270+
nwrittenOut prim.Pointer[prim.Usize],
2271+
) FastlyStatus
2272+
2273+
// GetTrailerValues returns an iterator that yields the values for the named
2274+
// header that are of the request.
2275+
func (r *HTTPBody) GetTrailerValues(name string) *Values {
2276+
adapter := func(
2277+
buf *prim.Char8,
2278+
bufLen prim.Usize,
2279+
cursor multiValueCursor,
2280+
endingCursorOut *multiValueCursorResult,
2281+
nwrittenOut *prim.Usize,
2282+
) FastlyStatus {
2283+
nameBuffer := prim.NewReadBufferFromString(name).ArrayU8()
2284+
2285+
return fastlyHTTPBodyTrailerValuesGet(
2286+
r.h,
2287+
nameBuffer.Data, nameBuffer.Len,
2288+
prim.ToPointer(buf), bufLen,
2289+
cursor,
2290+
prim.ToPointer(endingCursorOut),
2291+
prim.ToPointer(nwrittenOut),
2292+
)
2293+
}
2294+
2295+
return newValues(adapter, DefaultLargeBufLen) // Large enough to get most header values in a single call.
2296+
}
2297+
21312298
// witx:
21322299
//
21332300
// (module $fastly_http_resp

0 commit comments

Comments
 (0)