coverage~bigbes/sr-ht-ecore3bd158fbpages/error.go

Coverage
100.0% 21/21 statements
Δ
Blob
21392dc
Uncovered nothing — every instrumented line ran
1 package pages
2
3 import (
4 "embed"
5 "net/http"
6 )
7
8 // sharedFS holds this package's own templates: the error partial parsed into
9 // every set, and the error page used by a service that ships none.
10 //
11 //go:embed templates
12 var sharedFS embed.FS
13
14 const (
15 sharedDir = "templates"
16
17 // errorPartialFile defines ErrorPartial and nothing else, so that it can be
18 // parsed into every page set without bringing a content block with it.
19 errorPartialFile = "error.tmpl"
20
21 // errorPageFile is the default error page: a content block that is one
22 // invocation of ErrorPartial.
23 errorPageFile = "error.html"
24 )
25
26 // ErrorPage is the name Load registers the error page under, and the name to
27 // pass Render for it. It is a constant because a service's `fail` names it on
28 // every arm of its switch.
29 const ErrorPage = "error"
30
31 // ErrorPartial is the shared error body: the status line, the message and the
32 // way back. Its dot is an ErrorData — not a view struct — so a service that
33 // wants its own error.html around it invokes it as
34 // {{template "srht-error" .Data}}.
35 const ErrorPartial = "srht-error"
36
37 // The messages of the error pages a surface produces on its own. They are
38 // constants, and shared ones, for the reason the donors gave: the visibility
39 // rules of these services require "somebody else's private thing" and "no such
40 // thing" to be indistinguishable, and two 404s that differed in their prose
41 // would rebuild the distinction the status code was chosen to erase.
42 //
43 // A 400 has no constant deliberately. It is the one class that describes
44 // something the viewer just typed — "1y is not a duration", "grants are not
45 // lower case" — and an error page that replaced that with a house phrase would
46 // send them back to the form with nothing to change.
47 const (
48 NotFoundMessage = "There is nothing here."
49 UnauthorizedMessage = "You need to be logged in to do that."
50 ForbiddenMessage = "You may not do that."
51 MethodMessage = "That is not something you can do to this page."
52 InternalMessage = "Something went wrong on our side. It has been logged."
53 UnavailableMessage = "Something we depend on is not answering. Try again in a moment."
54 )
55
56 // Message is the standard message for a status, or "" for a status that has
57 // none — a 400 above all, whose message is the caller's own text.
58 // The machine-facing halves of the same table. A REST surface answers a caller
59 // that parses, not a person that reads, and every service on the instance keeps
60 // its 404 body byte-identical on purpose: two spellings of "not found" are two
61 // facts a client can accidentally distinguish, which is exactly what the shared
62 // status was chosen to prevent.
63 //
64 // They live beside the page sentences rather than in a second package because
65 // they are one table read two ways — and because the service that tried to
66 // reuse RenderRefusals on its REST surface could not, for want of these.
67 const (
68 APINotFoundMessage = "not found"
69 APIUnauthorizedMessage = "unauthorized"
70 APIForbiddenMessage = "forbidden"
71 APIMethodMessage = "method not allowed"
72 APIInternalMessage = "internal server error"
73 APIUnavailableMessage = "service unavailable"
74 )
75
76 // APIMessage is Message for a machine-facing surface: the same statuses, in the
77 // register a JSON client expects. An unmapped status returns "", which the
78 // caller renders as it likes — usually http.StatusText.
79 18 func APIMessage(status int) string {
80 18 switch status {
81 2 case http.StatusUnauthorized:
82 2 return APIUnauthorizedMessage
83 2 case http.StatusForbidden:
84 2 return APIForbiddenMessage
85 3 case http.StatusNotFound:
86 3 return APINotFoundMessage
87 2 case http.StatusMethodNotAllowed:
88 2 return APIMethodMessage
89 2 case http.StatusInternalServerError:
90 2 return APIInternalMessage
91 6 case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
92 6 return APIUnavailableMessage
93 1 default:
94 1 return ""
95 }
96 }
97
98 24 func Message(status int) string {
99 24 switch status {
100 2 case http.StatusUnauthorized:
101 2 return UnauthorizedMessage
102 2 case http.StatusForbidden:
103 2 return ForbiddenMessage
104 6 case http.StatusNotFound:
105 6 return NotFoundMessage
106 2 case http.StatusMethodNotAllowed:
107 2 return MethodMessage
108 3 case http.StatusInternalServerError:
109 3 return InternalMessage
110 7 case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
111 7 return UnavailableMessage
112 2 default:
113 2 return ""
114 }
115 }
116
117 // A Link is a href and the text that carries it.
118 type Link struct {
119 Href string
120 Text string
121 }
122
123 // DefaultBack is the way back off an error page for a service that names none:
124 // its own root, which every one of these services answers with a landing page.
125 var DefaultBack = Link{Href: "/", Text: "Back to the start"}
126
127 // ErrorData is the payload of the error page. A service puts it where its view
128 // struct keeps page payload — the field the layout's content sees as .Data in
129 // all of the donors:
130 //
131 // vd := s.view(r, http.StatusText(status))
132 // vd.Data = pages.Error(status, message).BackTo("/tokens", "Back to your tokens")
133 // err := s.pages.Render(w, status, pages.ErrorPage, vd)
134 type ErrorData struct {
135 Status int
136 StatusText string
137
138 // Message is what the viewer can act on. It is never the raw error from
139 // below: those name tables, queries and paths. It goes through
140 // html/template, which escapes it — which matters, because the messages
141 // that are not constants quote what the caller typed.
142 Message string
143
144 // Back is the way off this page. Empty renders no link at all rather than a
145 // link to nowhere, which is what a hand-built ErrorData would otherwise get.
146 Back Link
147 }
148
149 // Error builds the payload for a status. An empty message takes the standard
150 // one for that status (Message), so a caller that has nothing of its own to add
151 // says nothing rather than inventing a phrase.
152 9 func Error(status int, message string) ErrorData {
153 9 if message == "" {
154 7 message = Message(status)
155 7 }
156 9 return ErrorData{
157 9 Status: status,
158 9 StatusText: http.StatusText(status),
159 9 Message: message,
160 9 Back: DefaultBack,
161 9 }
162 }
163
164 // BackTo replaces the way back, for the services whose landing page is not "/"
165 // or whose word for it is not "back to the start". It returns a copy, so it
166 // chains off Error.
167 1 func (d ErrorData) BackTo(href, text string) ErrorData {
168 1 d.Back = Link{Href: href, Text: text}
169 1 return d
170 1 }