Skip to content

Commit dfceafd

Browse files
jphastingsStebalien
authored andcommitted
Gateway renders pretty 404 pages if available
In the same way that an `index.html` file is rendered, if one is present, when the requested path is a directory, now an `ipfs-404.html` file is rendered if the requested file is not present within the specified IPFS object. `ipfs-404.html` files are looked for in the directory of the requested path and each parent until one is found, falling back on the well-known 404 error message. License: MIT Signed-off-by: JP Hastings-Spital <[email protected]>
1 parent 043acbd commit dfceafd

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

core/corehttp/gateway_handler.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
gopath "path"
1212
"regexp"
1313
"runtime/debug"
14+
"strconv"
1415
"strings"
1516
"time"
1617

@@ -203,6 +204,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
203204
webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable)
204205
return
205206
default:
207+
if i.servePretty404IfPresent(w, r, parsedPath) {
208+
return
209+
}
210+
206211
webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusNotFound)
207212
return
208213
}
@@ -290,6 +295,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
290295
return
291296
}
292297

298+
if i.servePretty404IfPresent(w, r, parsedPath) {
299+
return
300+
}
301+
293302
// storage for directory listing
294303
var dirListing []directoryItem
295304
dirit := dir.Entries()
@@ -406,6 +415,36 @@ func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, nam
406415
http.ServeContent(w, req, name, modtime, content)
407416
}
408417

418+
func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, parsedPath ipath.Path) bool {
419+
resolved404Path, ctype, err := i.searchUpTreeFor404(r, parsedPath)
420+
if err != nil {
421+
return false
422+
}
423+
424+
dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
425+
if err != nil {
426+
return false
427+
}
428+
defer dr.Close()
429+
430+
f, ok := dr.(files.File)
431+
if !ok {
432+
return false
433+
}
434+
435+
size, err := f.Size()
436+
if err != nil {
437+
return false
438+
}
439+
440+
log.Debugf("using pretty 404 file for %s", parsedPath.String())
441+
w.Header().Set("Content-Type", ctype)
442+
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
443+
w.WriteHeader(http.StatusNotFound)
444+
_, err = io.CopyN(w, f, size)
445+
return err == nil
446+
}
447+
409448
func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
410449
p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body))
411450
if err != nil {
@@ -619,3 +658,45 @@ func getFilename(s string) string {
619658
}
620659
return gopath.Base(s)
621660
}
661+
662+
func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, parsedPath ipath.Path) (ipath.Resolved, string, error) {
663+
filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
664+
if err != nil {
665+
return nil, "", err
666+
}
667+
668+
pathComponents := strings.Split(parsedPath.String(), "/")
669+
670+
for idx := len(pathComponents); idx >= 3; idx-- {
671+
pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
672+
parsed404Path := ipath.New("/" + pretty404)
673+
if parsed404Path.IsValid() != nil {
674+
break
675+
}
676+
resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
677+
if err != nil {
678+
continue
679+
}
680+
return resolvedPath, ctype, nil
681+
}
682+
683+
return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
684+
}
685+
686+
func preferred404Filename(acceptHeaders []string) (string, string, error) {
687+
// If we ever want to offer a 404 file for a different content type
688+
// then this function will need to parse q weightings, but for now
689+
// the presence of anything matching HTML is enough.
690+
for _, acceptHeader := range acceptHeaders {
691+
accepted := strings.Split(acceptHeader, ",")
692+
for _, spec := range accepted {
693+
contentType := strings.SplitN(spec, ";", 1)[0]
694+
switch contentType {
695+
case "*/*", "text/*", "text/html":
696+
return "ipfs-404.html", "text/html", nil
697+
}
698+
}
699+
}
700+
701+
return "", "", fmt.Errorf("there is no 404 file for the requested content types")
702+
}

core/corehttp/gateway_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,70 @@ func TestGatewayGet(t *testing.T) {
235235
}
236236
}
237237

238+
func TestPretty404(t *testing.T) {
239+
ns := mockNamesys{}
240+
ts, api, ctx := newTestServerAndNode(t, ns)
241+
defer ts.Close()
242+
243+
f1 := files.NewMapDirectory(map[string]files.Node{
244+
"ipfs-404.html": files.NewBytesFile([]byte("Custom 404")),
245+
"deeper": files.NewMapDirectory(map[string]files.Node{
246+
"ipfs-404.html": files.NewBytesFile([]byte("Deep custom 404")),
247+
}),
248+
})
249+
250+
k, err := api.Unixfs().Add(ctx, f1)
251+
if err != nil {
252+
t.Fatal(err)
253+
}
254+
255+
host := "example.net"
256+
ns["/ipns/"+host] = path.FromString(k.String())
257+
258+
for _, test := range []struct {
259+
path string
260+
accept string
261+
status int
262+
text string
263+
}{
264+
{"/ipfs-404.html", "text/html", http.StatusOK, "Custom 404"},
265+
{"/nope", "text/html", http.StatusNotFound, "Custom 404"},
266+
{"/nope", "text/*", http.StatusNotFound, "Custom 404"},
267+
{"/nope", "*/*", http.StatusNotFound, "Custom 404"},
268+
{"/nope", "application/json", http.StatusNotFound, "ipfs resolve -r /ipns/example.net/nope: no link named \"nope\" under QmcmnF7XG5G34RdqYErYDwCKNFQ6jb8oKVR21WAJgubiaj\n"},
269+
{"/deeper/nope", "text/html", http.StatusNotFound, "Deep custom 404"},
270+
{"/deeper/", "text/html", http.StatusNotFound, "Deep custom 404"},
271+
{"/deeper", "text/html", http.StatusNotFound, "Deep custom 404"},
272+
{"/nope/nope", "text/html", http.StatusNotFound, "Custom 404"},
273+
} {
274+
var c http.Client
275+
req, err := http.NewRequest("GET", ts.URL+test.path, nil)
276+
if err != nil {
277+
t.Fatal(err)
278+
}
279+
req.Header.Add("Accept", test.accept)
280+
req.Host = host
281+
resp, err := c.Do(req)
282+
283+
if err != nil {
284+
t.Fatalf("error requesting %s: %s", test.path, err)
285+
}
286+
287+
defer resp.Body.Close()
288+
if resp.StatusCode != test.status {
289+
t.Fatalf("got %d, expected %d, from %s", resp.StatusCode, test.status, test.path)
290+
}
291+
body, err := ioutil.ReadAll(resp.Body)
292+
if err != nil {
293+
t.Fatalf("error reading response from %s: %s", test.path, err)
294+
}
295+
296+
if string(body) != test.text {
297+
t.Fatalf("unexpected response body from %s: got %q, expected %q", test.path, body, test.text)
298+
}
299+
}
300+
}
301+
238302
func TestIPNSHostnameRedirect(t *testing.T) {
239303
ns := mockNamesys{}
240304
ts, api, ctx := newTestServerAndNode(t, ns)

0 commit comments

Comments
 (0)