Help us shape the future of Go! Take the 2026 Go Developer Survey

response

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 14 Imported by: 0

README

HTTP Response Helpers

Use arkoren.dev/core/http/response to write a handler's final response. Once status or body bytes are accepted, a returned error cannot replace the response. Template helpers buffer execution before commitment. On success, StringTemplate sets text/plain; charset=utf-8 and HTMLTemplate sets text/html; charset=utf-8, replacing any existing Content-Type. Execution failures leave headers unchanged.

For streamed responses:

  • SSE normalizes CRLF and bare CR to LF. Trailing line breaks produce empty data: fields so EventSource retains the complete normalized payload.
  • Iterator byte slices are copied before yield returns. Producer panics are re-raised on the request goroutine after the producer exits.
  • Available flush failures are returned, including an unsupported flush. Even the initial flush follows response commitment. Errorless flushers cannot report their failures.
  • Iterator producers are joined, never abandoned. A producer blocked outside yield must observe cancellation/shutdown or have another bounded exit. A write or flush failure does not cancel its captured request context.

See the package API and examples for stream capability requirements and per-write deadline behavior.

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

Examples

Constants

This section is empty.

Variables

View Source
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
View Source
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, &lt;reader&gt;</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

func ValidateRedirectTarget(target string) error

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
}

Directories

Path Synopsis
internal
stream
Package stream owns producer joining and streaming response I/O.
Package stream owns producer joining and streaming response I/O.

Jump to

Keyboard shortcuts

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