http

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

Package http is the request and response layer a controller action works with: the incoming Request, the Response types a handler can build, and the Context an action is called with.

Sub-packages:

http/client     is an HTTP client for calling other services.
http/exceptions holds the exception types a response can carry.
http/middleware is the standard middleware every application wires.
http/resources  transforms data into response shapes.
http/testing    helps a test build requests and inspect responses.

The tenant never comes from the request

There is no method on Request that reads a tenant id out of a path parameter, a query string, a header or a body field, and adding one would be the most direct route to a cross-tenant leak. The tenant is on the auth.Grant the policy mints, and the repository reads it from there.

If a method here seems to offer tenant access -- Server, Header, Input -- it does not. They read what the browser sent; the tenant is what the Grant authorises, never what the request carries.

Net/http and the package name

The package is named http because it is the HTTP layer, and the directory is the component name. When it collides with net/http, the cost of the alias is on the caller:

import (
    "net/http"
    hhttp "github.com/arandu-io/hesape/http"
)

Inside this package, net/http is imported as stdhttp.

Index

Constants

View Source
const (
	// JSONHexTag is the "hex tag" encoding flag: escape "<" and ">".
	JSONHexTag = 1
	// JSONHexAmp is the "hex amp" encoding flag: escape "&".
	JSONHexAmp = 2
	// JSONHexApos is the "hex apos" encoding flag: escape "'".
	JSONHexApos = 4
	// JSONHexQuot is the "hex quot" encoding flag: escape the double quote.
	JSONHexQuot = 8
	// JSONForceObject forces an empty value or a map to encode as a JSON
	// object rather than an array.
	JSONForceObject = 16
	// JSONNumericCheck encodes a numeric string as a number.
	JSONNumericCheck = 32
	// JSONUnescapedSlashes and JSONUnescapedUnicode both mean "stop
	// rewriting characters that were already valid", which in encoding/json
	// is the one switch SetEscapeHTML: setting either flag turns it off.
	JSONUnescapedSlashes = 64
	// JSONPrettyPrint indents the encoded output.
	JSONPrettyPrint = 128
	// JSONUnescapedUnicode is documented on JSONUnescapedSlashes.
	JSONUnescapedUnicode = 256
	// JSONPartialOutputOnError falls back to an empty object instead of
	// failing when the value cannot be encoded. SetData reads it.
	JSONPartialOutputOnError = 512
	// JSONPreserveZeroFraction keeps a trailing ".0" on a float that has no
	// fractional part.
	JSONPreserveZeroFraction = 1024
	// JSONThrowOnError is accepted for compatibility; an encoding failure is
	// always reported through the returned error here.
	JSONThrowOnError = 4194304
)

The encoding flags are bit values JsonResponse takes as its options argument, and HasEncodingOption tests a caller's payload against. The numbers are fixed: a caller reads back a flag it set through HasEncodingOption, and renumbering the constants would answer a different question for anyone who already depends on the current values.

encoding/json has no flag word, so only the flags that change what Go can emit are honoured: JSONPrettyPrint indents, JSONUnescapedSlashes and JSONUnescapedUnicode turn off Go's HTML escaping, and JSONPartialOutputOnError is read by SetData when deciding whether an encoding failure is fatal. The rest are accepted, stored and reported by HasEncodingOption, and otherwise change nothing.

View Source
const IntendedCookieName = "arandu_intended"

IntendedCookieName carries the address somebody was going to, between the request a guard turned away and the sign-in that follows it.

Fixed, for the reason session.CookieName is fixed: the guard writes it and the sign-in handler reads it, and two parts of a project that disagree about the name is a person who signs in and lands on the front page with no explanation -- which is the thing this exists to remove.

It is a cookie and not a row: there is no session yet at the moment the guard fires, which is exactly why it fires, so there is nowhere on the server to put it that is keyed to this browser.

View Source
const IntendedLifetime = 10 * time.Minute

IntendedLifetime is how long the address is worth keeping.

Long enough to find a password in a manager and type it, and short enough that it does not survive to an unrelated sign-in: on a shared machine, an address kept for a day sends the next person who signs in to the page the previous one was refused.

Variables

View Source
var ErrFileNotFound = errors.New("http: the uploaded file is not readable")

ErrFileNotFound is returned when the file behind an upload is gone.

Functions

func Back

func Back(r *stdhttp.Request) string

Back is the address a rejected request is sent to: where it came from, or "/".

Exported because a handler that answers a rejection itself -- one that has a domain reason rather than a rule failure -- needs the same address, and a second reading of the Referer header is a second place for the open-redirect check to be missing.

It answers "/" for everything it cannot prove is ours: no Referer, an unparseable one, one on another host, or a path LocalPath refuses. Landing on the front page is a bad answer; landing on somebody else's site carrying the fact that you just submitted a form is a worse one.

func File

func File(c *Context, field string) (filesystem.Upload, error)

File is the one file that arrived in a form field.

It is the whole of what this package knows about multipart, and it exists so that the knowing happens once: hesape/filesystem.FromMultipart is waiting for a *multipart.FileHeader and had nothing handing it one, so every project would have written this call itself, and the ones that wrote it slightly differently would have differed on what a filename is.

It is a function taking a Context rather than a method for the reason filesystem.Upload is where it is: an upload is checked against filesystem.UploadRules and stored with a Disk, and none of that belongs on the request. The returned Upload carries nothing but a name, a size, an announced type and a way to open the bytes -- all of it the client's except the size.

The size limit is not here

The body is read into memory or into a temporary file before this returns, so the thing that stops a four gigabyte upload is middleware.LimitBodySize on the way in, not a rule checked on the way out. The temporary file, when there is one, is removed by net/http when the request ends -- so the bytes must be read during the request, which is what Disk.Put does.

func ForwardedForFrom

func ForwardedForFrom(ctx context.Context) []string

ForwardedForFrom returns the chain of client addresses on a request context, or nil when no trusted proxy put one there.

func LocalPath

func LocalPath(raw string) (string, bool)

LocalPath reports whether an address stays inside this application, and returns it when it does.

It is the open-redirect defence, and it is one function in the collection rather than one per caller: Reject calls it on the address a rejected form is sent back to, which comes off the Referer header and is therefore the visitor's to choose, and Intended calls it twice around a signed cookie. Two copies of this would be two lists of refused shapes, and the second list is always the shorter one.

A destination is accepted only as an absolute path on this origin. That is narrow on purpose: "is this URL one of ours" answered by comparing hosts is a question with a decade of published bypasses in it, and the only answer that has none is not to accept a host at all.

The four shapes it refuses, and what each one does:

  • anything not starting with "/": "https://evil.example/login" and "javascript:..." are both Location headers a browser will act on.
  • "//evil.example/x": a protocol-relative URL. It arrives here looking like a path -- net/http parses a request target beginning with "//" into URL.Path, host and all -- and a browser resolves it to another origin.
  • "/\evil.example/x": browsers normalise a backslash to a slash in the authority position, so this is the line above with one character changed, and it is the form that gets past a check written as "starts with a single slash".
  • a control character or a space anywhere in it: header injection, and the browsers that strip such characters before resolving, which turns a rejected address into an accepted one.

func MatchesType

func MatchesType(actual, typ string) bool

MatchesType reports whether two content types match, accounting for the "+suffix" vendor syntax. Accepts and Prefers both rely on it.

func Redirect

func Redirect(w stdhttp.ResponseWriter, r *stdhttp.Request, to string)

Redirect is Context.Redirect for the code that holds a raw ResponseWriter: a middleware, or a handler registered with Get rather than Action.

It is one function and not one per caller. The branch below existed in three copies -- here, in the auth module's handlers, and in the route guards -- and the middle one had already been wrong once, answering HX-Redirect with 200 to every client including the ones that do not read the header, which left a browser with scripts off on a blank page after signing in. Three copies of a six-line decision is three chances to be the copy that is wrong.

303 and not 302: after a POST, 303 is what tells the browser to GET the next address instead of posting the body to it again.

func Refuse

func Refuse(w stdhttp.ResponseWriter, r *stdhttp.Request, status int, message string)

Refuse answers a refusal that the person in front of the browser can see.

The status and the sentence are the caller's and are unchanged: 403 stays 403 and 419 stays 419, so logs, monitoring and every non-HTMX client see exactly what they saw before.

What it adds is the HTMX half. htmx does not swap a 4xx -- its response handling is configured `{code:"[45]..", swap:false, error:true}` in the copy this framework embeds -- so a guard's 403 and an expired CSRF token both arrived as a fired event and a blank screen: the person clicked, nothing at all happened, and the sentence explaining why was in a response body that was thrown away. HX-Refresh makes the browser reload the page as an ordinary navigation, and that request is not an HTMX one, so the same middleware answers the same refusal and the browser renders it. Nothing loops: a reload is a GET, which CSRF verification does not check, and which a role guard answers as a plain page.

What a reload can and cannot show

It reloads the page the person is ON, which is not always the address that was refused. When the two are the same -- an expired token on a form, a fragment of the page itself -- the refusal comes back as a full page and is read. When they differ -- a boosted link into an area this account may not open -- the person gets their own page back, correct and unexplained, and learns only that the link does nothing.

That half is deliberately not solved by sending them to the refused address instead: the address would come off the request, and a request target beginning with "//" is parsed into URL.Path host and all, so handing it to location.href is an open redirect built out of a refusal. Something visible and incomplete beats something invisible, and beats a hole.

It is one function and not a branch in each middleware, for the reason Redirect above is one function: the last time this decision existed in three copies, one of them was wrong.

func Reject

Reject answers a request whose input failed the rules: back where it came from, with the messages and with what was typed still in the boxes.

It composes what answering a rejected form needs: a redirect back to where the request came from, carrying the input that was typed and the validation messages, so the page that follows the redirect can show them. The shape is deliberate: the answer to a rejected form is a redirect, not a body. A body was tried first and is the failure that produced this file. A 422 carrying the messages in the markup is thrown away by htmx unless the layout has reconfigured its response handling, so the person saw the form they submitted, unchanged, with nothing on it; and even where it was swapped in, a reload re-posted the form. A redirect is read by every client there is, and the page that follows it is a page.

Why it takes the flash rather than reaching for one

RedirectResponse's WithInput and WithErrors need a session, and this package requires hesape/session for exactly that reason -- the dependency runs Http -> Session and never back. The Flash is a parameter and not a package-level default because a process serving two applications has two keys.

What "back" means, and why it is checked

The Referer, and "/" when there is none or it is not ours. That address ends up in a Location header, so it is chosen by whoever sent the request: "https://evil.example/login" is a Referer somebody can send, and answering it turns every form in the application into an open redirect. LocalPath is the check, and it is the same one the intended-destination cookie has used since it existed -- one definition of "is this address ours", carrying its four documented bypasses, rather than a second one written here that knows about three of them.

What goes in the flash

The posted form, minus the fields session.Flash never carries back, and the messages. The query string is deliberately not included: it is already in the address being returned to, and adding it would put the same values on the page twice with a chance of the two disagreeing.

func WithForwardedFor

func WithForwardedFor(parent context.Context, chain []string) context.Context

WithForwardedFor returns a context carrying the chain of client addresses the trust-proxies middleware worked out, nearest hop first.

The middleware calls it, because it is the only thing that knows which proxies are ours: the chain cannot be recovered from the request afterwards, which is why Request.IPs could not answer with one. It is exported for that, and for the test that drives a request past the middleware.

Only the first entry is verified. Everything to the left of it was written by whoever that is and can say anything at all, which is why Request.IP -- what a rate limit, a throttle or a log line is keyed by -- is the first and never the last.

func WithState

func WithState(parent context.Context, s State) context.Context

WithState returns a context carrying the framework's per-request state.

The session middleware calls it. It is exported for that, and for the test that drives a request past the middleware, and for nothing else.

Types

type Arrayable

type Arrayable interface {
	// ToArray is the map representation.
	ToArray() map[string]any
}

Arrayable is a value that knows how to present itself as a map. SetContent turns one into JSON, which is the whole reason it is named here.

type BaseFile

type BaseFile struct {
	// contains filtered or unexported fields
}

BaseFile is a file on disk.

It is BaseFile and not File because File is already the function that pulls an upload off a Context, and a package cannot have both.

func NewBaseFile

func NewBaseFile(pathname string) *BaseFile

NewBaseFile builds a BaseFile at a path.

func (*BaseFile) Dimensions

func (f *BaseFile) Dimensions() (int, int, bool)

Dimensions is the width and height of the image, and whether it is one.

Returns (width, height, ok): ok is false when the file is not a decodable image.

func (*BaseFile) Extension

func (f *BaseFile) Extension() string

Extension is an alias for GuessExtension.

func (*BaseFile) GetMimeType

func (f *BaseFile) GetMimeType() string

GetMimeType is the MIME type guessed from the extension.

func (*BaseFile) GetPathname

func (f *BaseFile) GetPathname() string

GetPathname is an alias for Path.

func (*BaseFile) GetRealPath

func (f *BaseFile) GetRealPath() string

GetRealPath resolves symlinks in the path; a path that cannot be resolved is returned as it stands, because the caller wanted a path and not an error about one.

func (*BaseFile) GetSize

func (f *BaseFile) GetSize() int64

GetSize is the file's size in bytes.

func (*BaseFile) GuessExtension

func (f *BaseFile) GuessExtension() string

GuessExtension is the extension guessed from the file's name, without the dot, empty when nothing is known. It guesses from the name because Go has no built-in content-sniffing equivalent to libmagic, and the name is what a person sees.

func (*BaseFile) HashName

func (f *BaseFile) HashName(dir ...string) string

HashName is a random 40-character name with the file's extension, under an optional directory.

The name is drawn once per file and cached, so a caller that asks twice stores and links the same thing. The variadic argument is an optional directory prefix.

func (*BaseFile) Path

func (f *BaseFile) Path() string

Path is the fully-qualified path to the file.

type Context

type Context struct {
	// Response and Request are exported: a handler that needs the standard
	// library reaches for it directly instead of waiting for a wrapper.
	Response stdhttp.ResponseWriter
	Request  *stdhttp.Request
	// contains filtered or unexported fields
}

Context is what a controller action receives.

It is everything a controller action gets, and no more: the request, the response, and helpers that answer. Nothing more -- and the "nothing more" is the point. There is no database handle here, no repository, no Grant. A controller that could reach the data layer would be a controller that skipped the service, and therefore the policy that guards it.

It is a struct rather than an interface because it has no second implementation and never will. One way to do one thing.

func NewContext

func NewContext(w stdhttp.ResponseWriter, r *stdhttp.Request, render Renderer, urls URLGenerator) *Context

NewContext builds the Context an action is called with.

It is exported because the thing that calls actions -- the router -- is no longer in this package, and the two fields it fills are unexported so that nothing else can swap a renderer or a URL table mid-request. hesape/routing calls it once per request; a test that drives an action directly calls it with nil for whichever of the two that action does not use.

func (*Context) BearerToken

func (c *Context) BearerToken() string

BearerToken is the token of an Authorization: Bearer header, or empty.

The scheme is compared case-insensitively because RFC 9110 says it is, and the value is trimmed because a client that sends two spaces is a client, not an attack.

func (*Context) Cookie

func (c *Context) Cookie(name string) string

Cookie is the value of a cookie, or empty when the browser sent none.

Empty rather than (string, bool): every caller of the two-value form in the framework threw the bool away, and a cookie that is present and empty means the same thing to all of them as a cookie that is absent.

func (*Context) Ctx

func (c *Context) Ctx() context.Context

Ctx returns the request context, which carries the Collector, the logger and the request id.

func (*Context) Fragment

func (c *Context) Fragment(status int, name string, data any) error

Fragment renders a partial with a status, for HTMX.

The status matters: a form that failed validation answers 422 with the form fragment, so the browser and the logs agree with each other. Answering 200 would make both of them believe it worked.

The layout has to let htmx swap it, and by default htmx does not

This comment used to end with "and HTMX swaps it in", stated as fact. It does not. htmx's default response handling is

[{code:"204", swap:false}, {code:"[23]..", swap:true}, {code:"[45]..", swap:false, error:true}]

in the copy this framework embeds (2.0.4), and a 422 matches the third entry. The fragment is fetched, the status is right, the body is correct -- and it is thrown away. The person sees the form they submitted, unchanged, with no message on it: the same failure that made a guard's 403 invisible, on the answer every form in an application gives.

Neither HX-Retarget nor HX-Reswap rescues it. Both are read after shouldSwap has already been decided from the table above; they change where and how, not whether. What decides whether is the configuration, and htmx reads it from the document without any script running:

<meta name="htmx-config" content='{"responseHandling":[
  {"code":"204","swap":false},
  {"code":"422","swap":true},
  {"code":"[23]..","swap":true},
  {"code":"[45]..","swap":false,"error":true}]}'>

422 before the catch-all, because htmx takes the first entry that matches. That line belongs in the application's layout, once -- it is the layout that decides what a fragment answer means, and a per-page opt-in would be a second way to answer a rejected form. A meta tag is not a script, so it costs nothing against a `script-src 'self'` policy and nothing in Node.

A refusal is a different thing and does not go through here: 403, 419 and 429 are not a form coming back with messages on it, and they answer through Refuse.

func (*Context) FullURL

func (c *Context) FullURL() string

FullURL is the address this request was made to, scheme and host included.

It is what a mail template needs, and what nothing that answers HTML needs: a link inside a page is a path, so that the application keeps working behind a proxy, on a staging host and on somebody's laptop.

The scheme is the one the browser used, which behind a proxy is not the one this process is listening on. middleware.TrustProxies is what records it; with no proxy in front, r.TLS answers it directly. A request whose scheme neither of those two can prove is reported as http, because guessing https for a listener that is plain is how a mail link becomes unreachable.

func (*Context) Header

func (c *Context) Header(name string) string

Header reads a request header, canonicalising the name like net/http does.

func (*Context) IP

func (c *Context) IP() string

IP is the address the request came from.

Behind a proxy it is whatever middleware.TrustProxies wrote onto RemoteAddr, and with no proxy it is the peer. Read it and do not parse a forwarding header here: which hop to believe is a deployment fact, it is decided once in that middleware, and a second reading of X-Forwarded-For is a second answer -- the one an attacker gets to choose.

It answers the address without the port, and tolerates a RemoteAddr that has none, because TrustProxies writes a bare address: a port that belonged to a connection this request did not arrive on is worse than no port.

func (*Context) Input

func (c *Context) Input(name string) string

Input reads a form field, from the body or the query string.

Named Input rather than Form because Input is the word the vocabulary already uses for it, and the vocabulary is the point.

func (*Context) IsHTMX

func (c *Context) IsHTMX() bool

IsHTMX reports whether htmx made this request.

It is the question that decides whether an answer is a fragment or a page, and it is asked in one place per decision: Redirect and Refuse already ask it for themselves, so a handler needs this only when the two shapes differ in what they render.

func (*Context) JSON

func (c *Context) JSON(status int, resource JsonResource) error

JSON answers with JSON. It exists for the endpoints that are genuinely an API; a page answers with View.

It takes a resource and not a value, and the signature is the whole point of it. An encoder handed an entity answers with whatever fields the entity happens to have, including the ones somebody adds to it later without ever reading this handler: a password hash, an internal note, the identifier of the account a row belongs to. A resource cannot do that, because what leaves is a list somebody wrote.

The answer is the fields under a "data" key, and whatever With returns beside it. The key is fixed here and does not follow the resource layer's configurable wrapping: this is one answer with one shape, and an endpoint that needs another builds its body with the resource layer and writes it to Context.Response itself.

A field carrying a value that reports itself missing is left out, which is what a conditional field is for -- the field a person may not see is absent rather than present and empty.

The body is built before anything is written. An encoder writing straight into the response has already sent the status and half an object by the time it reports that it cannot marshal the rest, and neither can be taken back.

func (*Context) Method

func (c *Context) Method() string

Method is the HTTP method, upper case: "GET", "POST".

func (*Context) Old

func (c *Context) Old(field string) string

Old returns what was typed in a field on the request that was rejected, or empty.

A view reads it through view.Page.OldValue rather than through here -- the page has the state on it by the time a view runs. This is for the handler that needs the value in Go: re-deriving a select's options from what was chosen, for instance.

It is always empty for a password field, by construction. See session.Flash.

func (*Context) Param

func (c *Context) Param(name string) string

Param reads a path parameter: /invoices/{id} gives Param("id").

func (*Context) Path

func (c *Context) Path() string

Path is the path of the request, without the query string.

func (*Context) Query

func (c *Context) Query(name string) string

Query reads a query string parameter.

func (*Context) Redirect

func (c *Context) Redirect(to string) error

Redirect answers a redirect, and does the right thing under HTMX.

An HTMX request that gets a 302 follows it inside the fragment, so the whole page ends up nested in a div. HX-Redirect is the header that makes the browser navigate instead. Handling it here means no application has to remember.

func (*Context) RedirectRoute

func (c *Context) RedirectRoute(name string, params ...string) error

RedirectRoute redirects to a named route, with its parameters filled in order.

It is Redirect over URL, and it exists so that the address a person is sent to after a POST is looked up by name like every other address in the application. An unknown name is logged by URL and answered as a redirect to "/", because the alternative -- a Location header that is empty -- is a browser that stays where it is with no explanation.

func (*Context) State

func (c *Context) State() State

State returns what the framework knows about this request.

func (*Context) Status

func (c *Context) Status(code int) error

Status answers with a status and no body.

func (*Context) URL

func (c *Context) URL(name string, params ...string) string

URL is the path of a named route, with its parameters filled in order.

ctx.URL("posts.show", post.ID)   -> "/posts/01J.../"

It is what a controller hands a view instead of building a path by hand. "/posts/"+id compiles and keeps compiling after the route moves; this stops working the moment the name is wrong, and says so.

An unknown name or a wrong number of parameters returns empty and is logged at ERROR with the name. Empty is what the views already treat as "there is no link here" -- a page with a missing button is recoverable, and a template renderer that panics takes the whole page down to report something a missing link would have said better.

func (*Context) User

func (c *Context) User() (auth.Subject, bool)

User is who is signed in, and whether anybody is.

It reads the subject the authentication middleware put on the context, and it is the only thing a controller may ask about identity. It is deliberately not a Grant: a Grant is minted by a policy for one action on one thing, and a controller that could produce one would be a controller that authorises itself.

func (*Context) View

func (c *Context) View(name string, data any) error

View renders a page. The data is a typed struct, never a map.

return ctx.View("invoices/index", IndexData{Invoices: list})

A map would compile and render blank on a typo, which is the failure this framework exists to make impossible.

func (*Context) WantsJSON

func (c *Context) WantsJSON() bool

WantsJSON reports whether this request asked for JSON rather than a page.

htmx is deliberately excluded even though it sends X-Requested-With: it swaps HTML, so answering it with JSON puts a JSON document inside a div.

hesape/exception states the same rule as the default of its Config.RenderJSONWhen, because it must answer a failure on a request that never reached a Context. The two are the same three lines and must not drift; folding them into one is waiting on the layer that wires the handler.

func (*Context) WithCookie

func (c *Context) WithCookie(cookie *stdhttp.Cookie) *Context

WithCookie writes a cookie on the answer and returns the Context, so that the line that sets one reads as one line:

return ctx.WithCookie(pref).Redirect("/settings")

It is a plain stdhttp.Cookie and not a struct of this package's own: the standard library's has every attribute there is, and a wrapper would have to be taught each new one.

type ExceptionHandler

type ExceptionHandler interface {
	// Report records the error wherever failures are recorded. It is separate
	// from Render because an error that was answered still has to be reported,
	// and a report that only happens when a page is drawn misses every failure
	// on a queued job.
	Report(ctx context.Context, err error)
	// Render writes the answer: the debug page in development, a status page
	// anywhere else.
	Render(w stdhttp.ResponseWriter, r *stdhttp.Request, err error)
}

ExceptionHandler turns an error that reached the edge into an answer.

Declared here and implemented in hesape/exception, which must not import this package: exception produces middleware through pipeline precisely so that the error path stays below the request path. The kernel wires the concrete one at boot, and hesape/routing calls it instead of panicking.

type Intended

type Intended struct {
	// contains filtered or unexported fields
}

Intended is where somebody was going when a guard turned them away.

It lives with the rest of what answers a request rather than with the session: the value it carries is an address, the whole of its correctness is LocalPath, and hesape/session never validates a URL.

It holds no state of its own -- the address is in a signed cookie in the browser -- so one value serves every request, and it is wired once at boot beside the Flash and the session store, over the same application key.

func NewIntended

func NewIntended(appKey []byte, secure bool) *Intended

NewIntended returns an Intended over the application key.

The same key as the session, the flash and the signed links, because they are the same secret: an attacker who has it does not need four. Pass secure=false only in development -- without the Secure attribute the cookie travels over plain HTTP, and it carries where somebody was going.

func (*Intended) Clear

func (i *Intended) Clear(w stdhttp.ResponseWriter)

Clear tells the browser to drop the address.

One function and not two copies of a cookie literal, because the two copies have to agree on Path: a clearing cookie written for a different path does not replace the one that is there, and the address quietly survives the thing that was supposed to spend it.

Intended.Take calls it, which spends the address. It is exported for the other caller, which is signing out: the address outlives a session by up to IntendedLifetime, and a shared machine changes hands at exactly that moment -- the next person to sign in would be carried to the page the previous one was refused. A sign-out handler calls it beside session.Invalidate.

func (*Intended) Remember

func (i *Intended) Remember(w stdhttp.ResponseWriter, r *stdhttp.Request)

Remember records where this request was going, so that the sign-in screen it is about to be sent to can finish the journey.

The guard is the only thing that knows what the person was reaching for, and by the time they have typed a password that request is gone. Without it every sign-in ends at the front page, and somebody who followed a link to one invoice has to find it again.

Why it is signed rather than merely same-site

The value decides where a browser goes immediately after authenticating, so whoever can write it can choose where every person in the application lands. SameSite=Strict stops another SITE from setting it, and does not stop another HOST on the same registrable domain: a cookie set on ".example.com" by anything holding a subdomain -- a customer's CNAME, a forgotten staging box, a vendor's status page -- arrives here indistinguishable from ours. An HMAC does stop it, because the attacker does not have the application key, and it comes with the expiry signed into the same bytes so a stale address cannot be replayed by keeping the cookie alive.

The destination is checked for being local anyway, on the way in and on the way out, because a signature only proves that WE wrote the value.

What it declines to remember

Only a GET, because the address is replayed by a browser navigation: a POST remembered here is a form submission turned into a link, which either answers 405 or, on a route that also accepts GET, performs something the person did not ask for a second time.

Only a whole page, never an HTMX fragment. A hx-get that is refused would otherwise be remembered as "/inbox/rows?page=2", and after signing in the person lands on that partial: no layout, no navigation, and it reads as a broken deploy. A boosted navigation is a page and is kept -- hx-boost is how most links in this stack are followed, so dropping it would drop nearly everything.

It writes nothing when there is nothing worth writing, so a caller has no branch to get wrong.

func (*Intended) Take

func (i *Intended) Take(w stdhttp.ResponseWriter, r *stdhttp.Request, fallback string) string

Take returns the address Remember stored, and clears it.

It is meant to be the whole of a sign-in handler's last line:

return ctx.Redirect(intended.Take(ctx.Response, ctx.Request, "/"))

The fallback is a parameter rather than a constant because it is the one part that genuinely differs -- a blog sends people to the front page, an application to its dashboard -- and taking it here means no caller has to write the branch for "there was nowhere in particular".

It answers the fallback for anything it cannot prove

A forged or foreign signature, an expired one, a value that is not a local address: all of them are somebody's redirect that is not ours, and none of them is worth telling the person about at the moment they have just signed in. The check that the address is local is what keeps this from being an open redirect -- "sign in and then continue to https://evil.example/login" is the oldest phishing link there is, and it belongs here rather than in each project's handler precisely because every project would otherwise have to remember it.

It is consumed, not read: the cookie is cleared whatever it said, so the address is used once. An address left standing is one a person meets again at the next sign-in from this browser, weeks later, with no idea why.

type JsonResource added in v0.7.0

type JsonResource interface {
	// ToArray returns the fields that may leave, by name.
	ToArray() map[string]any

	// With returns what goes beside them at the top level of the response:
	// metadata about the answer rather than the thing being answered with.
	With() map[string]any
}

JsonResource is what Context.JSON takes: the fields a type is allowed to answer with, declared once beside the type rather than remembered at each place it is answered from.

It is declared here rather than imported from the resource layer, which declares the same contract: that layer builds a response and imports this package to do it, so an import back would be a cycle. Go compares an interface by its methods, so a resource satisfies this one by being what it already is, and the two names are one contract rather than two.

type JsonResponse

type JsonResponse struct {
	Response
	// contains filtered or unexported fields
}

JsonResponse is a Response whose body is the JSON encoding of a value, with the encoding flags kept so that SetEncodingOptions can re-encode what is already there.

It embeds Response, so Status, Header, WithCookie, ThrowResponse and the rest are the same methods on the same fields.

func FromJsonString

func FromJsonString(data string, args ...any) (*JsonResponse, error)

FromJsonString builds a response around a string that is already encoded JSON.

The variadic arguments are status (200) and headers (none).

func NewJsonResponse

func NewJsonResponse(data any, args ...any) (*JsonResponse, error)

NewJsonResponse builds a JsonResponse.

The variadic arguments are, in order: status (200), headers (none), options (0) and json (false -- whether data is already an encoded JSON string).

Returns (*JsonResponse, error): the error is set when data cannot be encoded.

func (*JsonResponse) Cookie

func (r *JsonResponse) Cookie(cookie *stdhttp.Cookie) *JsonResponse

Cookie is Response.Cookie, typed for the chain.

func (*JsonResponse) GetData

func (r *JsonResponse) GetData(args ...any) (any, error)

GetData is the payload decoded back out of the encoded body.

The variadic args are accepted and ignored: they exist for a caller passing a decoding mode and a depth limit, neither of which encoding/json needs -- it always decodes into map[string]any and has no depth limit. An empty body decodes as (nil, nil).

func (*JsonResponse) GetEncodingOptions

func (r *JsonResponse) GetEncodingOptions() int

GetEncodingOptions is the encoding flags currently set.

func (*JsonResponse) HasEncodingOption

func (r *JsonResponse) HasEncodingOption(option int) bool

HasEncodingOption reports whether a flag is set in the encoding options.

func (*JsonResponse) Header

func (r *JsonResponse) Header(key string, values ...any) *JsonResponse

Header is Response.Header, typed for the chain.

func (*JsonResponse) SetCallback

func (r *JsonResponse) SetCallback(callback string) *JsonResponse

SetCallback sets the JSONP callback and rebuilds the body. WithCallback calls it.

func (*JsonResponse) SetData

func (r *JsonResponse) SetData(data any) (*JsonResponse, error)

SetData encodes the value and makes it the body. It keeps the value on original, which is what GetOriginalContent returns.

Returns an error when encoding fails, unless JSONPartialOutputOnError is set, in which case it falls back to an empty object instead.

func (*JsonResponse) SetEncodingOptions

func (r *JsonResponse) SetEncodingOptions(options int) (*JsonResponse, error)

SetEncodingOptions changes the flags and re-encodes what is already there.

func (*JsonResponse) SetStatusCode

func (r *JsonResponse) SetStatusCode(code int) *JsonResponse

SetStatusCode is Response.SetStatusCode, typed for the chain.

func (*JsonResponse) WithCallback

func (r *JsonResponse) WithCallback(callback string) *JsonResponse

WithCallback sets the JSONP callback.

func (*JsonResponse) WithCookie

func (r *JsonResponse) WithCookie(cookie *stdhttp.Cookie) *JsonResponse

WithCookie is Response.WithCookie, typed for the chain.

func (*JsonResponse) WithException

func (r *JsonResponse) WithException(err error) *JsonResponse

WithException is Response.WithException, typed for the chain.

func (*JsonResponse) WithHeaders

func (r *JsonResponse) WithHeaders(headers stdhttp.Header) *JsonResponse

WithHeaders is Response.WithHeaders, typed for the chain.

func (*JsonResponse) WithoutCookie

func (r *JsonResponse) WithoutCookie(name string, args ...string) *JsonResponse

WithoutCookie is Response.WithoutCookie, typed for the chain.

func (*JsonResponse) WithoutHeader

func (r *JsonResponse) WithoutHeader(keys ...string) *JsonResponse

WithoutHeader is Response.WithoutHeader, typed for the chain.

type JsonSerializable

type JsonSerializable interface {
	// JsonSerialize is the value that gets encoded in this one's place.
	JsonSerialize() any
}

JsonSerializable is a value that names what of itself is encoded. Go's encoding/json reaches the same end through json.Marshaler, which is checked too.

type Middleware

type Middleware = pipeline.Middleware[stdhttp.Handler]

Middleware is the standard net/http signature, named.

It is an alias and not a defined type, and the = is the whole point: hesape/session, hesape/cookie and hesape/exception all produce middleware, and none of them may import this package -- exception in particular is what answers a failure that never reached a Context. With an alias they return pipeline.Middleware[stdhttp.Handler] and it IS an http.Middleware; with a defined type each of them would have to import the layer that calls it, which is the cycle this collection is being reorganised to remove.

Being the standard func(stdhttp.Handler) stdhttp.Handler underneath is what keeps every middleware written for the Go ecosystem usable here unchanged. Note the one place that is not free: a []func(stdhttp.Handler) stdhttp.Handler is not assignable to a []Middleware, because a slice of an aliased element type converts and a slice type does not.

Compose with pipeline.Chain. There is no Chain here: a second one, differing only in being non-generic, is a second way to do the same thing.

type RedirectResponse

type RedirectResponse struct {
	Response
	// contains filtered or unexported fields
}

RedirectResponse is a Response whose whole content is a Location header, plus the four things a redirect carries across the request boundary -- flashed data, old input, errors and cookies.

It embeds Response, so its own methods and Response's are on the same fields.

The tenant is never flashed and never read back: WithInput carries what the browser typed, and what a repository is allowed to see comes from the auth.Grant a policy minted, not from a value that survived a redirect.

func NewRedirectResponse

func NewRedirectResponse(to string, args ...any) *RedirectResponse

NewRedirectResponse builds a RedirectResponse.

The variadic arguments are status (302) and headers (none).

func (*RedirectResponse) Cookie

func (r *RedirectResponse) Cookie(cookie *stdhttp.Cookie) *RedirectResponse

Cookie is Response.Cookie, typed for the chain.

func (*RedirectResponse) EnforceSameOrigin

func (r *RedirectResponse) EnforceSameOrigin(fallback string, args ...bool) *RedirectResponse

EnforceSameOrigin replaces the target with the fallback unless it is on the same origin as the request.

It is the open-redirect defence on the Response side, and it answers the same question LocalPath answers on the Request side -- by comparing hosts rather than by refusing a host at all. A framework redirect built from a path should still go through LocalPath; this is for the target that genuinely carries a host.

The variadic arguments are validateScheme and validatePort, both defaulting to true.

func (*RedirectResponse) ExceptInput

func (r *RedirectResponse) ExceptInput(keys ...string) *RedirectResponse

ExceptInput flashes everything except the named keys.

The keys are the form's own reason to drop a field. A password, a one-time code or a request token does not need naming here: the session store drops the secret fields whatever the caller passes.

func (*RedirectResponse) GetOriginalContent

func (r *RedirectResponse) GetOriginalContent() any

GetOriginalContent returns nil: a redirect has no body to have had an original.

func (*RedirectResponse) GetRequest

func (r *RedirectResponse) GetRequest() *Request

GetRequest is the request this redirect answers.

func (*RedirectResponse) GetSession

func (r *RedirectResponse) GetSession() *session.Store

GetSession is the session store this redirect flashes into.

func (*RedirectResponse) GetTargetUrl

func (r *RedirectResponse) GetTargetUrl() string

GetTargetUrl is where the browser is being sent.

func (*RedirectResponse) Header

func (r *RedirectResponse) Header(key string, values ...any) *RedirectResponse

Header is Response.Header, typed for the chain.

func (*RedirectResponse) OnlyInput

func (r *RedirectResponse) OnlyInput(keys ...string) *RedirectResponse

OnlyInput flashes only the named keys.

func (*RedirectResponse) SetRequest

func (r *RedirectResponse) SetRequest(request *Request) *RedirectResponse

SetRequest sets the request WithInput and OnlyInput read from.

func (*RedirectResponse) SetSession

func (r *RedirectResponse) SetSession(store *session.Store) *RedirectResponse

SetSession sets the session store With, WithInput and WithErrors flash into.

func (*RedirectResponse) SetTargetUrl

func (r *RedirectResponse) SetTargetUrl(to string) *RedirectResponse

SetTargetUrl sets where the browser is being sent. WithFragment and EnforceSameOrigin both call it.

func (*RedirectResponse) With

func (r *RedirectResponse) With(key any, value ...any) *RedirectResponse

With flashes a piece of data to the session, so the page that follows the redirect can read it once.

The key may be a map or a single key: a map flashes every pair in it, and a single key flashes with value. The value is variadic, defaulting to nil.

func (*RedirectResponse) WithCookie

func (r *RedirectResponse) WithCookie(cookie *stdhttp.Cookie) *RedirectResponse

WithCookie is Response.WithCookie, typed for the chain.

func (*RedirectResponse) WithCookies

func (r *RedirectResponse) WithCookies(cookies []*stdhttp.Cookie) *RedirectResponse

WithCookies adds several cookies at once.

func (*RedirectResponse) WithErrors

func (r *RedirectResponse) WithErrors(provider any, key ...string) *RedirectResponse

WithErrors flashes a container of messages into the session's error bag, so the page that follows draws them beside the fields they belong to.

provider takes any of several shapes: a MessageProvider, a *MessageBag, a map, a slice of strings, a single string, or validation.Errors, which is what this module's validator produces. The variadic key names the bag, defaulting to "default" -- which is what lets two forms on one page keep their messages apart.

func (*RedirectResponse) WithException

func (r *RedirectResponse) WithException(err error) *RedirectResponse

WithException is Response.WithException, typed for the chain.

func (*RedirectResponse) WithFragment

func (r *RedirectResponse) WithFragment(fragment string) *RedirectResponse

WithFragment puts a "#anchor" on the target, replacing one that is already there.

func (*RedirectResponse) WithHeaders

func (r *RedirectResponse) WithHeaders(headers stdhttp.Header) *RedirectResponse

WithHeaders is Response.WithHeaders, typed for the chain.

func (*RedirectResponse) WithInput

func (r *RedirectResponse) WithInput(input ...map[string]any) *RedirectResponse

WithInput flashes the input to the session so the form that was rejected comes back with the boxes filled.

It is one half of a pair with Request.Old, which reads what this writes. If the two diverge, the form comes back blank, which is the bug the pair exists to not have.

Two things never reach the session: an uploaded file, which is not something to put back in a text box, and a secret field, which the session store drops whatever the caller passes.

The variadic argument is the optional input to flash; with none, the request's own input is used.

func (*RedirectResponse) WithoutCookie

func (r *RedirectResponse) WithoutCookie(name string, args ...string) *RedirectResponse

WithoutCookie is Response.WithoutCookie, typed for the chain.

func (*RedirectResponse) WithoutFragment

func (r *RedirectResponse) WithoutFragment() *RedirectResponse

WithoutFragment drops any "#anchor" from the target.

func (*RedirectResponse) WithoutHeader

func (r *RedirectResponse) WithoutHeader(keys ...string) *RedirectResponse

WithoutHeader is Response.WithoutHeader, typed for the chain.

type Renderable

type Renderable interface {
	// Render draws the value and returns the result.
	Render() (string, error)
}

Renderable is a value that draws itself, a view being the one that matters. SetContent calls it rather than casting to string, so a failure inside a template is a failure and not an empty page.

Render returns (string, error): a rendering failure is reported through the error rather than by panicking.

type Renderer

type Renderer interface {
	Render(ctx context.Context, w stdhttp.ResponseWriter, status int, name string, data any) error
}

Renderer draws a named view with typed data.

It is an interface here, and implemented in the view package, for one reason: the view package imports http, so http importing the view package back would be a cycle. The kernel wires the concrete one at boot.

type Request

type Request struct {
	// contains filtered or unexported fields
}

Request wraps a *net/http.Request and provides the methods a controller action reaches for: the input the body and the query string carried, the headers, the cookies, the files, the session that survived the redirect, and the route that matched.

The tenant NEVER comes from the request. There is no method here that reads a tenant id out of a path parameter, a query string, a header or a body field, and adding one would be the most direct route to a cross-tenant leak. The tenant is on the auth.Grant the policy mints, and the repository reads it from there. If a method here seems to offer tenant access, it does not.

A note on names: method names use the initial uppercase Go requires. Input is Input, never Get. Old is Old, never Previous. Initialisms are upper case: FullURL, IsJSON. Where a failure can happen, the method returns (T, error).

func Capture

func Capture(r *stdhttp.Request) *Request

Capture returns NewRequest called on the given *http.Request.

func CreateFrom

func CreateFrom(from *Request) *Request

CreateFrom is a new Request built from another, sharing its query, body, headers, session and resolvers. It is what a middleware that needs a modified copy of the request uses.

func CreateFromBase

func CreateFromBase(r *stdhttp.Request) *Request

CreateFromBase returns NewRequest called on the given *http.Request.

func NewRequest

func NewRequest(r *stdhttp.Request) *Request

NewRequest wraps a *net/http.Request. It is the constructor a handler or a test calls; the router calls it once per request.

func (*Request) Accepts

func (r *Request) Accepts(contentTypes ...string) bool

Accepts reports whether the request accepts any of the given content types. An empty Accept header accepts everything.

func (*Request) AcceptsAnyContentType

func (r *Request) AcceptsAnyContentType() bool

AcceptsAnyContentType reports whether the request accepts any content type (empty Accept, "*/*" or "*").

func (*Request) AcceptsHTML

func (r *Request) AcceptsHTML() bool

AcceptsHTML reports whether the request accepts "text/html".

func (*Request) AcceptsJSON

func (r *Request) AcceptsJSON() bool

AcceptsJSON reports whether the request accepts "application/json".

func (*Request) Ajax

func (r *Request) Ajax() bool

Ajax reports whether the request was made by XMLHttpRequest. htmx sends this header too; see WantsJSON for why htmx is deliberately excluded there.

func (*Request) All

func (r *Request) All(keys ...string) map[string]any

All is the input and the files, merged. With keys, returns only those keys.

func (*Request) AllFiles

func (r *Request) AllFiles() map[string]any

AllFiles is every uploaded file, keyed by the form field name.

func (*Request) AnyFilled

func (r *Request) AnyFilled(keys ...string) bool

AnyFilled reports whether at least one key is filled.

func (*Request) Array

func (r *Request) Array(keys ...string) []any

Array is the value as a []any. When more than one key is given, returns Only for those keys.

func (*Request) BearerToken

func (r *Request) BearerToken() string

BearerToken is the token of an Authorization: Bearer header, or empty. The scheme is compared case-insensitively (RFC 9110) and the value is trimmed.

func (*Request) Boolean

func (r *Request) Boolean(key string, def ...bool) bool

Boolean is the value as a bool. Returns true for "1", "true", "on", "yes" (case-insensitive).

func (*Request) Collect

func (r *Request) Collect(keys ...string) []any

Collect is the value as a []any, which is the shape a hesape/collections.Collection is built from. With more than one key, returns Only for those keys as a slice.

func (*Request) Cookie

func (r *Request) Cookie(name string, def ...string) string

Cookie is the value of a cookie, or default when the browser sent none.

func (*Request) Date

func (r *Request) Date(key string, format ...string) (time.Time, bool)

Date is the value parsed as a time.Time. Without a format, parses as RFC3339; with a format, parses against it. Returns the zero time when the key is not filled or the value does not parse.

func (*Request) DecodedPath

func (r *Request) DecodedPath() string

DecodedPath is the path, URL-decoded.

func (*Request) Duplicate

func (r *Request) Duplicate(query, post, cookies, server map[string]any) *Request

Duplicate is a copy of the request with optional overrides for query, post, cookies and server. Nil arguments keep the original.

func (*Request) Enum

func (r *Request) Enum(key string, tryFrom func(string) (any, bool)) any

Enum is the value as an enum, via a tryFrom function the caller supplies. Returns nil when the key is not filled or the value does not match any enum case.

The caller passes the function that turns a string into the enum value and reports whether it matched. A Go enum that implements a TryFrom method can hand it directly:

status, ok := req.Enum("status", StatusTryFrom)

func (*Request) Except

func (r *Request) Except(keys ...string) map[string]any

Except is the input without the given keys.

func (*Request) ExpectsJSON

func (r *Request) ExpectsJSON() bool

ExpectsJSON reports whether the request probably expects a JSON response. True when the request is AJAX (not PJAX) and accepts any content type, or when it explicitly wants JSON.

func (*Request) File

func (r *Request) File(key string) any

File is the uploaded file(s) at the key, or nil. Returns *multipart.FileHeader, which UploadedFile wraps into the fuller file surface.

func (*Request) Filled

func (r *Request) Filled(keys ...string) bool

Filled reports whether every given key has a non-empty value. A boolean true and a non-empty list count as filled; an empty string or a string of whitespace does not.

func (*Request) FilterPrecognitiveRules

func (r *Request) FilterPrecognitiveRules(rules map[string]any) map[string]any

FilterPrecognitiveRules returns only the rules whose attribute matches one of the Precognition-Validate-Only header's patterns, when that header is present; otherwise it returns the rules unchanged.

func (*Request) Fingerprint

func (r *Request) Fingerprint() string

Fingerprint is a unique hash of the route methods, domain, URI and client IP. Panics when no route is set.

func (*Request) Flash

func (r *Request) Flash()

Flash flashes the request's input to the session, so the next request can read it as old input.

Panics when no session is set. A handler that calls this is inside a flow that started with a session, and a missing one there is a wiring error the caller should see immediately.

func (*Request) FlashExcept

func (r *Request) FlashExcept(keys ...string)

FlashExcept flashes the input except the given keys.

The keys are the form's own reason to drop a field -- a long body already saved elsewhere, a step the next page recomputes. A credential is not that reason and does not need naming here: the session store drops the secret fields whatever the caller passes.

func (*Request) FlashOnly

func (r *Request) FlashOnly(keys ...string)

FlashOnly flashes only some of the input.

func (*Request) Float

func (r *Request) Float(key string, def ...float64) float64

Float is the value as a float64.

func (*Request) Fluent

func (r *Request) Fluent(key string, def ...map[string]any) map[string]any

Fluent is the input as a map that reads with dot notation. It is the input source merged with the query, and a read on a missing key returns nil rather than panicking.

func (*Request) Flush

func (r *Request) Flush()

Flush removes all of the old input from the session.

func (*Request) Format

func (r *Request) Format(def ...string) string

Format is the response format the request prefers, derived from the Accept header. Returns the default ("html") when nothing matches.

func (*Request) FullURL

func (r *Request) FullURL() string

FullURL is the full URL, scheme and host included, with the query string.

func (*Request) FullURLIs

func (r *Request) FullURLIs(patterns ...string) bool

FullURLIs reports whether the full URL matches any of the patterns. "*" is the only wildcard.

func (*Request) FullURLWithQuery

func (r *Request) FullURLWithQuery(query map[string]string) string

FullURLWithQuery is the full URL with the given query parameters merged into the existing ones.

func (*Request) FullURLWithoutQuery

func (r *Request) FullURLWithoutQuery(keys ...string) string

FullURLWithoutQuery is the full URL without the given query parameters.

func (*Request) GetAcceptableContentTypes

func (r *Request) GetAcceptableContentTypes() []string

GetAcceptableContentTypes is the content types the Accept header lists, in priority order, without quality parameters.

func (*Request) GetRouteResolver

func (r *Request) GetRouteResolver() func() Route

GetRouteResolver is the installed resolver, or a no-op when none was set.

func (*Request) GetSession

func (r *Request) GetSession() *session.Store

GetSession is the session store. Panics when none is set. It is an alias for Session.

func (*Request) GetUserResolver

func (r *Request) GetUserResolver() func(guard string) any

GetUserResolver is the installed resolver, or a no-op when none was set.

func (*Request) HTTPHost

func (r *Request) HTTPHost() string

HTTPHost is the host as the browser sent it, port included when the browser sent one.

func (*Request) Has

func (r *Request) Has(keys ...string) bool

Has reports whether every given key exists in the input.

func (*Request) HasAny

func (r *Request) HasAny(keys ...string) bool

HasAny reports whether at least one of the keys exists.

func (*Request) HasCookie

func (r *Request) HasCookie(name string) bool

HasCookie reports whether a cookie is present.

func (*Request) HasFile

func (r *Request) HasFile(key string) bool

HasFile reports whether a valid file is present at the key.

func (*Request) HasHeader

func (r *Request) HasHeader(name string) bool

HasHeader reports whether a header is present, including when its value is empty.

func (*Request) HasSession

func (r *Request) HasSession() bool

HasSession reports whether a session store was set.

func (*Request) Header

func (r *Request) Header(name string, def ...string) string

Header is the first value of a header, or default when the header is absent. The name is canonicalised by net/http.

func (*Request) Host

func (r *Request) Host() string

Host is the host name, without the port.

func (*Request) IP

func (r *Request) IP() string

IP is the client address, without the port. Behind a proxy it is whatever the trust-proxies middleware wrote onto RemoteAddr.

func (*Request) IPs

func (r *Request) IPs() []string

IPs is the chain of client addresses, nearest hop first. With no proxy in front, this is a list of one.

It is the X-Forwarded-For entries plus the address the request actually came from, with our own proxies removed, reversed so that IPs()[0] is IP(). It used to return the one address and no chain, so behind a proxy sending "X-Forwarded-For: 1.1.1.1, 2.2.2.2" it answered with a single element instead of two: the doc said "the chain of client addresses" and there was never a chain.

Only the first entry is trustworthy. See WithForwardedFor.

func (*Request) Input

func (r *Request) Input(key string, def ...any) any

Input is a value from the request's input source merged with the query string, read with dot notation. The input source is the JSON body for JSON requests, the post form for POST/PUT/PATCH/DELETE, and the query string for GET/HEAD. The input source takes precedence over the query.

A JSON body is read the same way as form input: Input("user.name") descends by dot through the decoded payload, so a nested JSON field is found the same way a flattened form field is.

With no key, returns the whole merged map.

func (*Request) Instance

func (r *Request) Instance() *Request

Instance is the request itself.

func (*Request) Integer

func (r *Request) Integer(key string, def ...int64) int64

Integer is the value as an int64.

func (*Request) Is

func (r *Request) Is(patterns ...string) bool

Is reports whether the decoded path matches any of the patterns. "*" is the only wildcard.

func (*Request) IsAttemptingPrecognition

func (r *Request) IsAttemptingPrecognition() bool

IsAttemptingPrecognition reports whether the Precognition header is "true".

func (*Request) IsJSON

func (r *Request) IsJSON() bool

IsJSON reports whether the Content-Type header indicates JSON. The check is for "/json" or "+json" anywhere in the content type, so "application/json", "application/vnd.api+json" and "application/problem+json" all match.

func (*Request) IsMethod

func (r *Request) IsMethod(method string) bool

IsMethod reports a case-insensitive method match.

func (*Request) IsNotFilled

func (r *Request) IsNotFilled(keys ...string) bool

IsNotFilled reports whether every given key is empty.

func (*Request) IsPrecognitive

func (r *Request) IsPrecognitive() bool

IsPrecognitive reports whether the request was marked as a precognitive validation request.

func (*Request) Json

func (r *Request) Json(key string, def ...any) any

Json is the decoded JSON body. With a key, returns the value at the dotted path; without, returns the whole payload.

func (*Request) Keys

func (r *Request) Keys() []string

Keys is the keys of all input and files, merged.

func (*Request) Merge

func (r *Request) Merge(input map[string]any) *Request

Merge merges the given input into the request's input source, overwriting existing keys. Returns the request, so calls can chain.

func (*Request) MergeIfMissing

func (r *Request) MergeIfMissing(input map[string]any) *Request

MergeIfMissing merges the given input only for keys that are missing from the request.

func (*Request) Method

func (r *Request) Method() string

Method is the HTTP method, upper case.

func (*Request) Missing

func (r *Request) Missing(keys ...string) bool

Missing is the negation of Has.

func (*Request) Old

func (r *Request) Old(key string, def ...any) any

Old is the value that was typed into the form that was rejected, so the page it was sent back to can fill the boxes again. With no key, returns all of the old input; with a key, returns the value at that key or the default.

It is what makes a form come back pre-filled after a rejection, and its pair with RedirectResponse.WithInput is the two ends of one thing: WithInput writes into the session's old input, and Old reads it. If the two diverge, the form comes back blank, which is the bug this exists to not have.

Returns the default when no session is set.

func (*Request) Only

func (r *Request) Only(keys ...string) map[string]any

Only is a subset of the input containing only the given keys.

func (*Request) PJAX

func (r *Request) PJAX() bool

PJAX reports whether the request carries the X-PJAX header.

func (*Request) Path

func (r *Request) Path() string

Path is the path without leading or trailing slashes, "/" when the path is empty.

func (*Request) Post

func (r *Request) Post(key string, def ...string) string

Post is a post body parameter, or a default when it is absent. With an empty key, returns empty.

func (*Request) PreferSafeContent

func (r *Request) PreferSafeContent() bool

PreferSafeContent reports whether the Prefer header asks for safe content.

func (*Request) Prefers

func (r *Request) Prefers(contentTypes ...string) string

Prefers is the most suitable content type among the given ones, based on content negotiation. Returns empty when none match.

func (*Request) Prefetch

func (r *Request) Prefetch() bool

Prefetch reports whether the request is a browser prefetch.

func (*Request) Query

func (r *Request) Query(key string, def ...string) string

Query is a query string parameter, or a default when it is absent. With an empty key, returns empty.

func (*Request) RawRequest

func (r *Request) RawRequest() *stdhttp.Request

RawRequest returns the *net/http.Request this wraps, for the rare case that needs something Request's own methods do not cover. It is not the common path: the methods on Request are what a controller should reach for.

func (*Request) Replace

func (r *Request) Replace(input map[string]any) *Request

Replace replaces the input source entirely.

func (*Request) Root

func (r *Request) Root() string

Root is the root URL, scheme and host, without a trailing slash.

func (*Request) Route

func (r *Request) Route(args ...any) any

Route is the matched route, or a specific parameter from it. With no arguments, returns the Route. With a string argument, returns the parameter of that name from the route. With a default, returns the default when the parameter is absent.

Returns nil when no resolver is set.

func (*Request) RouteIs

func (r *Request) RouteIs(patterns ...string) bool

RouteIs reports whether the route name matches any of the patterns. Returns false when no route is set.

func (*Request) SchemeAndHttpHost

func (r *Request) SchemeAndHttpHost() string

SchemeAndHttpHost is scheme + "://" + host.

func (*Request) Secure

func (r *Request) Secure() bool

Secure reports whether the request was over TLS.

func (*Request) Segment

func (r *Request) Segment(index int, def ...string) string

Segment is the nth segment of the path (1-based), or default when the index is out of range.

func (*Request) Segments

func (r *Request) Segments() []string

Segments is the non-empty parts of the decoded path, split on "/".

func (*Request) Server

func (r *Request) Server(key string, def ...string) string

Server is a server variable, or default. The common CGI-style keys are mapped from the request; an unknown HTTP_* key falls back to the corresponding header.

func (*Request) Session

func (r *Request) Session() *session.Store

Session is the session store. Panics when none is set.

func (*Request) SetDefaultRequestLocale

func (r *Request) SetDefaultRequestLocale(locale string) *Request

SetDefaultRequestLocale stores the default locale on the request, for the view layer to fall back to.

func (*Request) SetJson

func (r *Request) SetJson(payload map[string]any) *Request

SetJson replaces the cached JSON payload.

func (*Request) SetLaravelSession

func (r *Request) SetLaravelSession(s *session.Store) *Request

SetLaravelSession is an alias for SetSession.

func (*Request) SetPrecognitive

func (r *Request) SetPrecognitive() *Request

SetPrecognitive marks the request as precognitive, which IsPrecognitive then reports.

func (*Request) SetRequestLocale

func (r *Request) SetRequestLocale(locale string) *Request

SetRequestLocale stores the locale on the request for the view layer to read. It is a string because Go has no locale type, and the view layer reads it through the same string.

func (*Request) SetRouteResolver

func (r *Request) SetRouteResolver(resolver func() Route) *Request

SetRouteResolver sets the resolver Route calls.

func (*Request) SetSession

func (r *Request) SetSession(s *session.Store) *Request

SetSession sets the session store.

func (*Request) SetUserResolver

func (r *Request) SetUserResolver(resolver func(guard string) any) *Request

SetUserResolver sets the resolver User calls.

func (*Request) Str

func (r *Request) Str(key string, def ...string) string

Str is an alias for String.

func (*Request) String

func (r *Request) String(key string, def ...string) string

String is the value as a string.

func (*Request) ToArray

func (r *Request) ToArray() map[string]any

ToArray is all input and files as a map.

func (*Request) URI

func (r *Request) URI() string

URI is the full URL string.

func (*Request) URL

func (r *Request) URL() string

URL is the request URL without the query string. It is the address a link inside the page uses, so that the application keeps working behind a proxy, on a staging host and on somebody's laptop.

*http.Request carries scheme and host in URL when it was parsed from a full target, and in Host when it arrived over the wire. Both are checked, so a test that builds a request from "http://example.com/path" and a server that builds one from "/path" both get the right answer.

func (*Request) User

func (r *Request) User(guard ...string) any

User is the authenticated user, via the resolver the auth middleware installed. Returns nil when no resolver is set.

func (*Request) UserAgent

func (r *Request) UserAgent() string

UserAgent is the User-Agent header, or empty.

func (*Request) Validate

func (r *Request) Validate(rules *validation.Set, opts ...validation.ValidatorOption) (validation.Input, error)

Validate runs the rules against the request's input and returns the validated values, or the error the failures make.

The rules are a compiled *validation.Set, built at boot with validation.MustCompile. The options carry the context, the Grant and the collaborators the rules that leave the process need.

func (*Request) ValidateWithBag

func (r *Request) ValidateWithBag(bag string, rules *validation.Set, opts ...validation.ValidatorOption) (validation.Input, error)

ValidateWithBag is Validate with the failures named, so that two forms on one page do not draw each other's errors.

func (*Request) WantsJSON

func (r *Request) WantsJSON() bool

WantsJSON reports whether the first acceptable content type is JSON.

func (*Request) WhenFilled

func (r *Request) WhenFilled(key string, callback func(value any) any, def ...func() any) any

WhenFilled calls the callback with the value when the key is filled, otherwise calls the default.

func (*Request) WhenHas

func (r *Request) WhenHas(key string, callback func(value any) any, def ...func() any) any

WhenHas calls the callback with the value when the key exists, otherwise calls the default. Returns the callback's result, or the request itself when the callback returns nil.

func (*Request) WhenMissing

func (r *Request) WhenMissing(key string, callback func(value any) any, def ...func() any) any

WhenMissing calls the callback when the key is missing, otherwise calls the default.

type Response

type Response struct {
	// contains filtered or unexported fields
}

Response is a status, a set of headers, a set of cookies and a body, built before anything is written to the wire.

It is a value and not a stdhttp.ResponseWriter: a controller returns it, middleware may still add a header to it, and Response.Send is the one place it meets the standard library.

func NewResponse

func NewResponse(content any, args ...any) (*Response, error)

NewResponse builds a Response.

Returns (*Response, error): the error is set when the content cannot be encoded. The variadic arguments are status 200 and no headers.

func (*Response) Content

func (r *Response) Content() string

Content is an alias for GetContent.

func (*Response) Cookie

func (r *Response) Cookie(cookie *stdhttp.Cookie) *Response

Cookie is an alias for WithCookie.

func (*Response) Cookies

func (r *Response) Cookies() []*stdhttp.Cookie

Cookies returns the cookies this response carries. Response.Send writes them.

func (*Response) Exception

func (r *Response) Exception() error

Exception returns the error WithException recorded.

func (*Response) GetCallback

func (r *Response) GetCallback() string

GetCallback is the JSONP callback, empty when there is none. Only a JsonResponse ever sets one.

func (*Response) GetContent

func (r *Response) GetContent() string

GetContent is the encoded body, empty when there is none.

func (*Response) GetOriginalContent

func (r *Response) GetOriginalContent() any

GetOriginalContent is what SetContent was handed, before it became JSON or HTML. A Response wrapping a Response unwraps.

func (*Response) GetProtocolVersion

func (r *Response) GetProtocolVersion() string

GetProtocolVersion is the HTTP protocol version string.

func (*Response) GetStatusCode

func (r *Response) GetStatusCode() int

GetStatusCode is an alias for Status.

func (*Response) Header

func (r *Response) Header(key string, values ...any) *Response

Header sets a header, replacing what is there unless replace is false.

func (*Response) Headers

func (r *Response) Headers() stdhttp.Header

Headers is the response's headers. It is a getter because a Go field would let a caller swap the whole map.

func (*Response) Send

func (r *Response) Send(w stdhttp.ResponseWriter) error

Send writes the status, the headers, the cookies and the body to the wire.

It is the one place this type meets stdhttp.ResponseWriter, which is the point of building a Response at all: everything before this is a value that middleware may still change.

func (*Response) ServeHTTP

func (r *Response) ServeHTTP(w stdhttp.ResponseWriter, _ *stdhttp.Request)

ServeHTTP makes a Response a stdhttp.Handler, so a route may answer with one directly. It is Send with the arguments a handler is given.

func (*Response) SetContent

func (r *Response) SetContent(content any) (*Response, error)

SetContent sets the body, encoding as needed.

It keeps what it was handed on original: a value that is "JSONable" -- Arrayable, Jsonable, JsonSerializable, json.Marshaler, a map or a slice -- becomes JSON and sets the Content-Type; a Renderable is rendered; anything else is cast to a string.

Returns (*Response, error): the error is set when JSON encoding fails.

func (*Response) SetProtocolVersion

func (r *Response) SetProtocolVersion(version string) *Response

SetProtocolVersion sets the HTTP protocol version string. NewResponse calls it with "1.0".

func (*Response) SetStatusCode

func (r *Response) SetStatusCode(code int) *Response

SetStatusCode sets the status code.

func (*Response) Status

func (r *Response) Status() int

Status is the status code.

func (*Response) StatusText

func (r *Response) StatusText() string

StatusText is the reason phrase.

func (*Response) ThrowResponse

func (r *Response) ThrowResponse() error

ThrowResponse wraps this response in an HttpResponseException so the layer above sends it instead of rendering.

The error is returned rather than thrown; the caller returns it in turn.

func (*Response) WithCookie

func (r *Response) WithCookie(cookie *stdhttp.Cookie) *Response

WithCookie adds a cookie.

func (*Response) WithException

func (r *Response) WithException(err error) *Response

WithException records the error that produced this answer.

func (*Response) WithHeaders

func (r *Response) WithHeaders(headers stdhttp.Header) *Response

WithHeaders adds several headers at once.

func (*Response) WithoutCookie

func (r *Response) WithoutCookie(name string, args ...string) *Response

WithoutCookie expires a cookie when the response is sent. MaxAge -1 is what tells a browser to delete it immediately.

The variadic arguments are an optional path and domain.

func (*Response) WithoutHeader

func (r *Response) WithoutHeader(keys ...string) *Response

WithoutHeader removes headers.

type Route

type Route interface {
	RouteName() string
	Parameter(name string, def ...any) any
	Parameters() map[string]any
	Methods() []string
	Domain() string
	URI() string
}

Route is the minimal view of the matched route the request needs.

The concrete type is hesape/routing.Route. The interface is declared here so that hesape/http does not import hesape/routing, which would be a cycle: routing builds the request, and the request needs the route's name and parameters, not the router itself.

type State

type State struct {
	// Errors is what the request that redirected here failed validation on,
	// keyed by the name of the form input.
	Errors validation.Errors
	// Old is what was typed on that request, minus every password field. See
	// session.Flash for what "every password field" means and why the messages
	// for those fields survive when their values do not.
	Old url.Values
}

State is what the framework knows about a request before the handler runs.

Today it is one thing: what the request that redirected here failed on. It is a struct rather than that one thing so the next piece of per-request framework state -- there will be one -- does not add a second context key, a second middleware and a second accessor that every page has to be taught about.

It is put on the request context by the middleware that spends the flash cookie, in hesape/session, and read by view.New. Nothing else writes it, and no handler ever has to: a handler that had to carry the errors from the middleware to the page would be a handler that can forget to, and forgetting is invisible -- the form comes back blank, exactly as it did before any of this existed.

func StateFrom

func StateFrom(ctx context.Context) State

StateFrom returns the state on a request context, or the zero State.

The zero value is the answer for every request that was not redirected here from a rejection, which is nearly all of them: no errors and no old input is a page that draws neither, not a page that has to check first.

type StoreOptions

type StoreOptions struct {
	// Disk is the disk to write to. The zero value is the default disk.
	Disk *filesystem.Disk
	// Visibility sets the stored file's visibility. StorePublicly and
	// StorePubliclyAs set it to "public".
	Visibility string
}

StoreOptions is what Store and its three siblings take to say where and how to store a file: a struct says this without a caller having to know a set of map keys by name.

type StreamedEvent

type StreamedEvent struct {
	// Event is the name of the event.
	Event string
	// Data is the payload.
	Data any
}

StreamedEvent is one named event on a server-sent event stream.

func NewStreamedEvent

func NewStreamedEvent(event string, data any) *StreamedEvent

NewStreamedEvent builds a StreamedEvent.

func (*StreamedEvent) String

func (e *StreamedEvent) String() string

String formats the event the way the server-sent event wire format wants it: an "event:" line, one "data:" line per line of payload, and a blank line.

It is here and not in a writer of its own because StreamedEvent is two fields and nothing else, and the framework that reads it needs the bytes.

type URLGenerator

type URLGenerator interface {
	Route(name string, params ...string) (string, error)
}

URLGenerator turns the name of a route into its path.

It is an interface here for the reason Renderer is: the route table lives in hesape/routing, which imports this package to build a Context, so naming its type here would be a cycle. hesape/routing.Routes satisfies it.

The method is Route and not URL because that is what the table calls it, and because Context.URL is the caller-facing name -- one word per side, and neither of them shadows net/url.

type UploadedFile

type UploadedFile struct {
	BaseFile
	// contains filtered or unexported fields
}

UploadedFile is one file that arrived in a form field, with the file-info methods and the four store variants.

It embeds BaseFile, so the two share the same methods on the same fields.

Storing one needs a Grant

The store methods take a context and an auth.Grant, because filesystem.Disk.PutFileAs does: a file is written under a tenant, the tenant comes off the Grant a policy minted, and there is no path from a form field to a storage prefix.

func NewUploadedFile

func NewUploadedFile(header *multipart.FileHeader, field string) *UploadedFile

NewUploadedFile builds an UploadedFile from the multipart part a browser sent.

This is the one constructor: a second package-level CreateFromBase for this type would collide with Request's, since Go namespaces by package rather than by type.

func NewUploadedFileFromPath

func NewUploadedFileFromPath(pathname, originalName, mimeType string, test bool) *UploadedFile

NewUploadedFileFromPath builds an UploadedFile standing in for one that arrived, from a file that is already on disk -- the shape a testing factory uses. The test flag is what IsValid checks instead of looking for a real multipart part.

func (*UploadedFile) ClientExtension

func (f *UploadedFile) ClientExtension() string

ClientExtension is an alias for GuessClientExtension.

func (*UploadedFile) Extension

func (f *UploadedFile) Extension() string

Extension is an alias for GuessExtension.

func (*UploadedFile) Get

func (f *UploadedFile) Get() ([]byte, error)

Get is the contents of the uploaded file.

Returns ([]byte, error): the error unwraps to ErrFileNotFound when the file is not valid.

func (*UploadedFile) GetClientMimeType

func (f *UploadedFile) GetClientMimeType() string

GetClientMimeType is the type the client announced, which is a header somebody wrote.

func (*UploadedFile) GetClientOriginalExtension

func (f *UploadedFile) GetClientOriginalExtension() string

GetClientOriginalExtension is the extension of the name the client announced.

func (*UploadedFile) GetClientOriginalName

func (f *UploadedFile) GetClientOriginalName() string

GetClientOriginalName is the name the client announced.

func (*UploadedFile) GetError

func (f *UploadedFile) GetError() int

GetError is the upload error code, 0 when there is none.

func (*UploadedFile) GetSize

func (f *UploadedFile) GetSize() int64

GetSize is the byte count the server counted as the body arrived, which is the one value on an upload that did not come from the client.

func (*UploadedFile) GuessClientExtension

func (f *UploadedFile) GuessClientExtension() string

GuessClientExtension is the extension guessed from the type the client announced.

func (*UploadedFile) GuessExtension

func (f *UploadedFile) GuessExtension() string

GuessExtension is the extension guessed from the announced name, since the pathname of an upload is that name.

func (*UploadedFile) HashName

func (f *UploadedFile) HashName(dir ...string) string

HashName is redeclared here so that the extension comes from the upload's own GuessExtension rather than the embedded BaseFile's.

func (*UploadedFile) IsValid

func (f *UploadedFile) IsValid() bool

IsValid reports whether the upload completed and there are bytes behind it.

func (*UploadedFile) Open

func (f *UploadedFile) Open() (io.ReadCloser, error)

Open returns a reader over the bytes. It is what Get and the store methods read through, and it may be called more than once.

func (*UploadedFile) Store

func (f *UploadedFile) Store(ctx context.Context, g auth.Grant, directory string, options StoreOptions) (string, error)

Store writes the file under a directory with a random name, and returns the key it landed on.

Returns (string, error). The context and the Grant are what filesystem.Disk.PutFileAs requires: a file is stored under the tenant a policy authorised, never under one read off the request.

func (*UploadedFile) StoreAs

func (f *UploadedFile) StoreAs(ctx context.Context, g auth.Grant, directory, name string, options StoreOptions) (string, error)

StoreAs writes the file under a directory with the name given, and returns the key it landed on.

Returns (string, error).

func (*UploadedFile) StorePublicly

func (f *UploadedFile) StorePublicly(ctx context.Context, g auth.Grant, directory string, options StoreOptions) (string, error)

StorePublicly is Store with the visibility set to public.

func (*UploadedFile) StorePubliclyAs

func (f *UploadedFile) StorePubliclyAs(ctx context.Context, g auth.Grant, directory, name string, options StoreOptions) (string, error)

StorePubliclyAs is StoreAs with the visibility set to public.

func (*UploadedFile) Upload

func (f *UploadedFile) Upload(field string) filesystem.Upload

Upload converts this file into the filesystem.Upload the storage layer takes. It is the one place the two vocabularies meet, so that the conversion is written once.

Directories

Path Synopsis
Package client is an HTTP client wrapping net/http.Client, with a testing surface built in: stub callbacks (Fake), response sequences, request recording, and assertions on what was sent.
Package client is an HTTP client wrapping net/http.Client, with a testing surface built in: stub callbacks (Fake), response sequences, request recording, and assertions on what was sent.
concerns
Package concerns is intentionally empty.
Package concerns is intentionally empty.
events
Package events holds the events dispatched during an HTTP client request lifecycle: RequestSending, ResponseReceived and ConnectionFailed.
Package events holds the events dispatched during an HTTP client request lifecycle: RequestSending, ResponseReceived and ConnectionFailed.
promises
Package promises provides the two promise-like types the HTTP client returns from an asynchronous request: FluentPromise and LazyPromise.
Package promises provides the two promise-like types the HTTP client returns from an asynchronous request: FluentPromise and LazyPromise.
Package concerns is intentionally empty.
Package concerns is intentionally empty.
Package exceptions holds the exception types an HTTP response can carry: HttpResponseException, MalformedUrlException, OriginMismatchException, PostTooLargeException and ThrottleRequestsException.
Package exceptions holds the exception types an HTTP response can carry: HttpResponseException, MalformedUrlException, OriginMismatchException, PostTooLargeException and ThrottleRequestsException.
Package middleware holds the standard middleware every application wires: CORS, security headers, body-size limits, host and proxy trust, and cache validation.
Package middleware holds the standard middleware every application wires: CORS, security headers, body-size limits, host and proxy trust, and cache validation.
Package resources transforms data into the shapes a JSON response takes: conditional fields, wrapping, pagination metadata and resource collections.
Package resources transforms data into the shapes a JSON response takes: conditional fields, wrapping, pagination metadata and resource collections.
json
Package json holds the concrete builders and response types that compose with the interfaces in the parent package hesape/http/resources.
Package json holds the concrete builders and response types that compose with the interfaces in the parent package hesape/http/resources.
jsonapi
Package jsonapi provides the resource types and helpers for building JSON:API-compliant responses, including sparse fieldsets, included resources, and relationship resolution.
Package jsonapi provides the resource types and helpers for building JSON:API-compliant responses, including sparse fieldsets, included resources, and relationship resolution.
Package testing helps a test build an UploadedFile without a real browser: a fake file of a given size or with given content, a fake image, and the MIME type lookups uploads are validated against.
Package testing helps a test build an UploadedFile without a real browser: a fake file of a given size or with given content, a fake image, and the MIME type lookups uploads are validated against.

Jump to

Keyboard shortcuts

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