package pages import ( "embed" "net/http" ) // sharedFS holds this package's own templates: the error partial parsed into // every set, and the error page used by a service that ships none. // //go:embed templates var sharedFS embed.FS const ( sharedDir = "templates" // errorPartialFile defines ErrorPartial and nothing else, so that it can be // parsed into every page set without bringing a content block with it. errorPartialFile = "error.tmpl" // errorPageFile is the default error page: a content block that is one // invocation of ErrorPartial. errorPageFile = "error.html" ) // ErrorPage is the name Load registers the error page under, and the name to // pass Render for it. It is a constant because a service's `fail` names it on // every arm of its switch. const ErrorPage = "error" // ErrorPartial is the shared error body: the status line, the message and the // way back. Its dot is an ErrorData — not a view struct — so a service that // wants its own error.html around it invokes it as // {{template "srht-error" .Data}}. const ErrorPartial = "srht-error" // The messages of the error pages a surface produces on its own. They are // constants, and shared ones, for the reason the donors gave: the visibility // rules of these services require "somebody else's private thing" and "no such // thing" to be indistinguishable, and two 404s that differed in their prose // would rebuild the distinction the status code was chosen to erase. // // A 400 has no constant deliberately. It is the one class that describes // something the viewer just typed — "1y is not a duration", "grants are not // lower case" — and an error page that replaced that with a house phrase would // send them back to the form with nothing to change. const ( NotFoundMessage = "There is nothing here." UnauthorizedMessage = "You need to be logged in to do that." ForbiddenMessage = "You may not do that." MethodMessage = "That is not something you can do to this page." InternalMessage = "Something went wrong on our side. It has been logged." UnavailableMessage = "Something we depend on is not answering. Try again in a moment." ) // Message is the standard message for a status, or "" for a status that has // none — a 400 above all, whose message is the caller's own text. // The machine-facing halves of the same table. A REST surface answers a caller // that parses, not a person that reads, and every service on the instance keeps // its 404 body byte-identical on purpose: two spellings of "not found" are two // facts a client can accidentally distinguish, which is exactly what the shared // status was chosen to prevent. // // They live beside the page sentences rather than in a second package because // they are one table read two ways — and because the service that tried to // reuse RenderRefusals on its REST surface could not, for want of these. const ( APINotFoundMessage = "not found" APIUnauthorizedMessage = "unauthorized" APIForbiddenMessage = "forbidden" APIMethodMessage = "method not allowed" APIInternalMessage = "internal server error" APIUnavailableMessage = "service unavailable" ) // APIMessage is Message for a machine-facing surface: the same statuses, in the // register a JSON client expects. An unmapped status returns "", which the // caller renders as it likes — usually http.StatusText. func APIMessage(status int) string { switch status { case http.StatusUnauthorized: return APIUnauthorizedMessage case http.StatusForbidden: return APIForbiddenMessage case http.StatusNotFound: return APINotFoundMessage case http.StatusMethodNotAllowed: return APIMethodMessage case http.StatusInternalServerError: return APIInternalMessage case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout: return APIUnavailableMessage default: return "" } } func Message(status int) string { switch status { case http.StatusUnauthorized: return UnauthorizedMessage case http.StatusForbidden: return ForbiddenMessage case http.StatusNotFound: return NotFoundMessage case http.StatusMethodNotAllowed: return MethodMessage case http.StatusInternalServerError: return InternalMessage case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout: return UnavailableMessage default: return "" } } // A Link is a href and the text that carries it. type Link struct { Href string Text string } // DefaultBack is the way back off an error page for a service that names none: // its own root, which every one of these services answers with a landing page. var DefaultBack = Link{Href: "/", Text: "Back to the start"} // ErrorData is the payload of the error page. A service puts it where its view // struct keeps page payload — the field the layout's content sees as .Data in // all of the donors: // // vd := s.view(r, http.StatusText(status)) // vd.Data = pages.Error(status, message).BackTo("/tokens", "Back to your tokens") // err := s.pages.Render(w, status, pages.ErrorPage, vd) type ErrorData struct { Status int StatusText string // Message is what the viewer can act on. It is never the raw error from // below: those name tables, queries and paths. It goes through // html/template, which escapes it — which matters, because the messages // that are not constants quote what the caller typed. Message string // Back is the way off this page. Empty renders no link at all rather than a // link to nowhere, which is what a hand-built ErrorData would otherwise get. Back Link } // Error builds the payload for a status. An empty message takes the standard // one for that status (Message), so a caller that has nothing of its own to add // says nothing rather than inventing a phrase. func Error(status int, message string) ErrorData { if message == "" { message = Message(status) } return ErrorData{ Status: status, StatusText: http.StatusText(status), Message: message, Back: DefaultBack, } } // BackTo replaces the way back, for the services whose landing page is not "/" // or whose word for it is not "back to the start". It returns a copy, so it // chains off Error. func (d ErrorData) BackTo(href, text string) ErrorData { d.Back = Link{Href: href, Text: text} return d }