Documentation
¶
Overview ¶
Package response writes HTTP responses from handlers: JSON, XML, HTML, redirects, streams, and server-sent events.
It provides focused helpers for common response types while keeping standard net/http response semantics.
Stream and SSE copy yielded bytes; SSEJSON encodes each value before releasing the yield callback. They re-raise producer panics on the calling goroutine. They stop iteration on cancellation, shutdown, or write or flush failure, then wait for the producer to exit. Code blocked outside yield must arrange its own bounded exit; graceful shutdown does not cancel the request context. See Stream for cancellation and shutdown responsibilities. Flush errors exposed by the writer are returned after commitment; the router cannot append a replacement problem response to the stream.
Example:
func ready(w http.ResponseWriter, req *http.Request) error {
chunks := func(yield func([]byte, error) bool) {
yield([]byte("ready"), nil)
}
return response.Stream(w, req, chunks)
}
Initialization behavior ¶
This package has no global setup and can be used directly from handlers.
Index ¶
- Variables
- func Bytes(writer http.ResponseWriter, status int, data []byte) error
- func HTML(writer http.ResponseWriter, status int, data string) error
- func HTMLTemplate(writer http.ResponseWriter, status int, ...) error
- func JSON[T any](writer http.ResponseWriter, status int, data T) error
- func Raw(writer http.ResponseWriter, status int, data []byte) error
- func Redirect(writer http.ResponseWriter, status int, target string) error
- func SSE(writer http.ResponseWriter, request *http.Request, ...) error
- func SSEChan(writer http.ResponseWriter, request *http.Request, values <-chan []byte, ...) error
- func SSEJSON[T any](writer http.ResponseWriter, request *http.Request, values iter.Seq2[T, error], ...) error
- func SafeRedirect(writer http.ResponseWriter, status int, target string) error
- func Status(writer http.ResponseWriter, status int) error
- func Stream(writer http.ResponseWriter, request *http.Request, ...) error
- func StreamChan(writer http.ResponseWriter, request *http.Request, values <-chan []byte, ...) error
- func String(writer http.ResponseWriter, status int, data string) error
- func StringTemplate(writer http.ResponseWriter, status int, responseTemplate *template.Template, ...) error
- func ValidateRedirectTarget(target string) error
- func XML(writer http.ResponseWriter, status int, data any) error
- type StreamOption
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNonFlushableWriter = errors.New("non-flushable response writer")
ErrNonFlushableWriter is returned when the http.ResponseWriter does not implement the http.Flusher interface, which is required for streaming responses. This capability check runs before a stream commits or starts its producer. An advertised capability that later fails returns that flush error instead.
Example:
err := response.StreamChan(w, req, chunks)
if errors.Is(err, response.ErrNonFlushableWriter) {
return response.String(w, http.StatusNotImplemented, "streaming unavailable")
}
return err
var ErrUnsafeRedirect = problem.New(func(details *problem.Builder) { details.Type("tag:arkoren.dev,2026-09:core:http/response:unsafe-redirect") details.Title("Invalid redirect target") details.Detail("The redirect target must be a safe relative path.") details.Status(http.StatusBadRequest) })
ErrUnsafeRedirect is returned by SafeRedirect when the target URL is not a safe relative path.
Example:
failure := response.ErrUnsafeRedirect.Derive(func(details *problem.Builder) {
details.Detail("The request cannot be completed.")
})
_ = failure.Status()
Functions ¶
func Bytes ¶
func Bytes(writer http.ResponseWriter, status int, data []byte) error
Bytes writes binary content as application/octet-stream, replacing any existing Content-Type. It follows Raw's commitment and write-error rules.
Example:
return response.Bytes(w, http.StatusOK, []byte("download"))
func HTML ¶
func HTML(writer http.ResponseWriter, status int, data string) error
HTML writes trusted HTML with Content-Type text/html; charset=utf-8. It does not escape input; use HTMLTemplate for user-controlled template data. It follows Raw's commitment and write-error behavior.
Example:
return response.HTML(w, http.StatusOK, "<p>Ready</p>")
func HTMLTemplate ¶
func HTMLTemplate(writer http.ResponseWriter, status int, responseTemplate *htmltemplate.Template, data any) error
HTMLTemplate executes an escaping html/template template into a buffer before writing it. Execution failures leave the response uncommitted. Successful execution sets Content-Type to text/html; charset=utf-8, replacing any existing value, and follows Raw's commitment and write-error behavior.
Example:
page, err := htmltemplate.New("greeting").Parse("<p>Hello, {{.}}</p>")
if err != nil {
return err
}
return response.HTMLTemplate(w, http.StatusOK, page, "reader")
Example ¶
ExampleHTMLTemplate escapes user data before committing the response.
package main
import (
"fmt"
htmltemplate "html/template"
"net/http"
"net/http/httptest"
"arkoren.dev/core/http/response"
)
func main() {
page, err := htmltemplate.New("greeting").Parse("<p>Hello, {{.}}</p>")
if err != nil {
fmt.Println(err)
return
}
writer := httptest.NewRecorder()
if err := response.HTMLTemplate(writer, http.StatusOK, page, "<reader>"); err != nil {
fmt.Println(err)
return
}
fmt.Println(writer.Header().Get("Content-Type"))
fmt.Println(writer.Body.String())
}
Output: text/html; charset=utf-8 <p>Hello, <reader></p>
func JSON ¶
func JSON[T any](writer http.ResponseWriter, status int, data T) error
JSON serializes and writes JSON followed by a newline. Encoding uses encoding/json/v2 and finishes before commitment; an encoding failure leaves the router able to render an error. Successful encoding sets Content-Type to application/json and follows Raw's write-error behavior.
Example:
return response.JSON(w, http.StatusOK, map[string]string{"status": "ready"})
func Raw ¶
func Raw(writer http.ResponseWriter, status int, data []byte) error
Raw writes raw byte data to the response writer with the specified status code. If no Content-Type header has been explicitly set, it defaults to application/octet-stream to ensure a safe content type is always present. A failed write is returned after commitment; already accepted bytes are not undone. The input is used during the call and is not retained.
Example:
w.Header().Set("Content-Type", "image/png")
return response.Raw(w, http.StatusOK, png)
func Redirect ¶
func Redirect(writer http.ResponseWriter, status int, target string) error
Redirect writes a redirect status and Location header. It does not validate target. Use SafeRedirect for user-controlled values; callers choose the redirect status and this helper writes no response body.
Example:
return response.Redirect(w, http.StatusSeeOther, "/dashboard")
func SSE ¶
func SSE(writer http.ResponseWriter, request *http.Request, values iter.Seq2[[]byte, error], opts ...StreamOption) error
SSE streams the values of a sequence as server-sent events. Each value becomes one event: every line of the value is sent as a "data:" field. CRLF and bare CR are normalized to LF; all LF characters, including trailing ones, are preserved in the EventSource data. Empty values emit an empty event. Options, deadline, error and shutdown behavior are those of Stream.
Example:
if err := response.SSE(w, req, events); err != nil {
return err
}
Example ¶
ExampleSSE preserves the final payload newline with an empty data field.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"arkoren.dev/core/http/response"
)
func main() {
values := func(yield func([]byte, error) bool) {
yield([]byte("first\n"), nil)
}
writer := httptest.NewRecorder()
err := response.SSE(writer, httptest.NewRequest(http.MethodGet, "/", nil), values)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%q\n", writer.Body.String())
}
Output: "data: first\ndata: \n\n"
func SSEChan ¶
func SSEChan(writer http.ResponseWriter, request *http.Request, values <-chan []byte, opts ...StreamOption) error
SSEChan is SSE for a channel of values, ending as StreamChan does.
Example:
err := response.SSEChan(w, req, events)
if err != nil {
return err
}
func SSEJSON ¶
func SSEJSON[T any](writer http.ResponseWriter, request *http.Request, values iter.Seq2[T, error], opts ...StreamOption) error
SSEJSON streams the values of a sequence as server-sent events, encoding each value as JSON (encoding/json/v2) into the "data:" field of one event. A value that cannot be encoded ends the stream with that error. Everything else behaves as SSE.
Example:
type tick struct{ N int }
ticks := func(yield func(tick, error) bool) {
for n := range 3 {
if !yield(tick{N: n}, nil) {
return
}
}
}
err := response.SSEJSON(w, req, ticks)
if err != nil {
return err
}
func SafeRedirect ¶
func SafeRedirect(writer http.ResponseWriter, status int, target string) error
SafeRedirect sends an HTTP redirect response only if the target is a safe relative path. Rejection returns ErrUnsafeRedirect before commitment; success follows Redirect. The path must start with one slash and contain no backslashes.
Example:
return response.SafeRedirect(w, http.StatusSeeOther, request.QueryOr(r, "next", "/"))
func Status ¶
func Status(writer http.ResponseWriter, status int) error
Status sets the HTTP status code for the response without writing any body content. This is useful for responses that communicate only a status. The response is committed even though no body bytes are written.
Example:
return response.Status(w, http.StatusNoContent)
func Stream ¶
func Stream(writer http.ResponseWriter, request *http.Request, values iter.Seq2[[]byte, error], opts ...StreamOption) error
Stream sends the values of a sequence to an HTTP client using streaming. Headers are sent immediately and each value is flushed as it is produced. The server write deadline is replaced by a fresh one before every write (see WithWriteTimeout, 30 seconds by default), so a stream outlives the server WriteTimeout. The deadline bounds supported network writes, not the time spent waiting for the producer to exit.
The sequence is drained in a goroutine so the stream can still react to the request context and to [server.ShuttingDown]. Each yielded slice is copied before yield returns; the producer may then reuse its storage. Producer panics are re-raised on the calling goroutine after the producer exits, so request recovery middleware can handle them.
Stream returns nil when the sequence ends or the serving [server.Server] starts shutting down, the context's error when the request is canceled, the sequence's error when it yields one, and the error of a failed write or flush, including by deadline. It returns ErrNonFlushableWriter if the writer does not implement http.Flusher. Once started, the producer is always joined. A deadline-setting error other than http.ErrNotSupported is returned before the affected flush or write. An initial deadline error leaves the response uncommitted; a later error does not append a replacement error response.
Flushes use http.ResponseController.Flush, preserving errors exposed by a writer's FlushError method, including http.ErrNotSupported. Writers exposing only the errorless Flush method cannot report flush failures. The initial flush follows WriteHeader, so even an initial flush failure occurs after commitment and cannot be replaced by a router error response. A failed data flush does not undo bytes already accepted by Write.
On early termination, yield returns false and Stream joins the producer; it cannot interrupt code blocked outside yield. Such producers must watch both request.Context().Done() and server.ShuttingDown(request.Context()), or have another bounded way to exit. Graceful shutdown does not cancel the request context, and a write or flush failure does not cancel it either. A producer that does not finish can therefore keep Stream waiting indefinitely. Because headers are already sent when the sequence fails, the router cannot render the returned error; it can only log it.
Example:
func letters(w http.ResponseWriter, req *http.Request) error {
chunks := func(yield func([]byte, error) bool) {
for _, chunk := range [][]byte{[]byte("a"), []byte("b")} {
if !yield(chunk, nil) {
return
}
}
}
return response.Stream(w, req, chunks)
}
func StreamChan ¶
func StreamChan(writer http.ResponseWriter, request *http.Request, values <-chan []byte, opts ...StreamOption) error
StreamChan is Stream for a channel of values. It returns when the channel is closed, the request context is canceled, or the serving [server.Server] starts shutting down (a clean end with a nil error). The producer must stop on its own, for example on the request context, so it does not block on send.
Example:
err := response.StreamChan(w, req, chunks)
if err != nil {
return err
}
func String ¶
func String(writer http.ResponseWriter, status int, data string) error
String writes text with Content-Type text/plain; charset=utf-8, replacing any existing Content-Type. It does not validate or transform the supplied bytes and follows Raw's write-error behavior.
Example:
return response.String(w, http.StatusOK, "pong")
func StringTemplate ¶
func StringTemplate(writer http.ResponseWriter, status int, responseTemplate *template.Template, data any) error
StringTemplate executes a text/template template into a buffer before writing it. Execution failures leave the response uncommitted. Successful execution sets Content-Type to text/plain; charset=utf-8, replacing any existing value, and follows Raw's commitment and write-error behavior.
Example:
page, err := template.New("greeting").Parse("Hello, {{.}}")
if err != nil {
return err
}
return response.StringTemplate(w, http.StatusOK, page, "reader")
Example ¶
ExampleStringTemplate renders plain text before commitment.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"text/template"
"arkoren.dev/core/http/response"
)
func main() {
page, err := template.New("greeting").Parse("Hello, {{.}}")
if err != nil {
fmt.Println(err)
return
}
writer := httptest.NewRecorder()
if err := response.StringTemplate(writer, http.StatusOK, page, "reader"); err != nil {
fmt.Println(err)
return
}
fmt.Println(writer.Header().Get("Content-Type"))
fmt.Println(writer.Body.String())
}
Output: text/plain; charset=utf-8 Hello, reader
func ValidateRedirectTarget ¶
ValidateRedirectTarget returns ErrUnsafeRedirect unless target is a safe relative path. It prevents open redirects when target originates from user input, such as a query parameter or form field. Only a path starting with exactly one slash is accepted. Backslashes, schemes, hosts, and URL parse failures are rejected without writing a response.
Example:
if err := response.ValidateRedirectTarget(next); err != nil {
return err
}
return response.Redirect(w, http.StatusSeeOther, next)
func XML ¶
func XML(writer http.ResponseWriter, status int, data any) error
XML serializes and writes XML followed by a newline. Encoding finishes before commitment; an encoding failure leaves the response uncommitted. Successful encoding sets Content-Type to application/xml and follows Raw's write-error behavior.
Example:
type result struct {
Status string `xml:"status"`
}
return response.XML(w, http.StatusOK, result{Status: "ready"})
Types ¶
type StreamOption ¶
type StreamOption func(*streamConfig)
StreamOption changes one setting of a Stream or SSE call. Obtain options through WithWriteTimeout; they apply to that call only and do not alter the producer or cancel its captured request context.
Example:
options := []response.StreamOption{response.WithWriteTimeout(time.Minute)}
return response.SSE(w, req, events, options...)
func WithWriteTimeout ¶
func WithWriteTimeout(d time.Duration) StreamOption
WithWriteTimeout sets how long each write and flush of the stream, and the initial header flush, may block before the connection write fails. The deadline is renewed before every write rather than bounding the whole stream. Iterator forms still wait for the producer to exit, as described by Stream; a write deadline cannot interrupt producer code blocked outside yield. Zero or negative uses 30 seconds. It has no effect on writers without write deadline support, which keep the server's own WriteTimeout. Deadline-setting errors matching http.ErrNotSupported are tolerated; other deadline-setting errors end the stream without losing their identity. Flush errors are always returned, including errors matching http.ErrNotSupported.
Example:
if err := response.SSE(w, req, events, response.WithWriteTimeout(time.Minute)); err != nil {
return err
}