Skip to content

DirectoryHTTPHandler cannot serve a directory URL as its index.html #240

Description

@vdhamer

DirectoryHTTPHandler cannot serve a directory URL as its index.html

Version: FlyingFox 0.27.1
Type: feature request

Summary

DirectoryHTTPHandler maps a request path onto a file under root and serves that file. It has no
notion of a directory index, so a request for /docs/ — a directory URL — resolves to the directory
itself, HTTPBodySequence(file:) fails, and the handler returns 404. Serving /docs/index.html
requires the client to spell out the filename.

index.html does not currently appear anywhere in FlyingFox/Sources.

Relevance

Serving a directory of static files is one of the things FlyingFox is most obviously good for, and
any site produced by a static-site generator (Ignite, Publish, Jekyll, Hugo, Eleventy) is laid out as
section/index.html and links to it as /section/. Those links are not something the site author
can avoid: they are how generators emit clean URLs, and they are what <a href="/en/clubs/"> in the
generated HTML says.

The result is that DirectoryHTTPHandler handles almost the whole job for the static-site case and
then has to be replaced wholesale, because the one missing piece is in the path resolution rather
than in the response. Everything after resolution — content type, Content-Length, ETag and
Last-Modified with 304 handling, range requests — is already there and is the part that would be
tedious to reimplement.

Reproducing

Build/
  index.html
  en/
    clubs/
      index.html
let server = HTTPServer(port: 8080, handler: DirectoryHTTPHandler(root: buildDirectory))
Request Now Expected with a directory index
GET /en/clubs/index.html 200 200
GET /en/clubs/ 404 200, serving en/clubs/index.html
GET / 404 200, serving index.html

Suggested API

An opt-in parameter, so current behaviour is untouched for anyone who does not ask for it:

public init(root: URL,
            serverPath: String = "/",
            directoryIndex: String? = nil,      // e.g. "index.html"
            cacheControl: [HTTPCacheControl.ResponseDirective] = [.private])

In makeFileURL(for:), when directoryIndex is set and the resolved URL is a directory, append the
index filename; if that file does not exist, return nil as today (404) rather than inventing a
directory listing. A listing is a separate feature with its own security and formatting questions,
and it is not what the static-site case needs.

Two details worth settling in the design:

  • Redirect or serve? Most servers 301 /docs to /docs/ so that relative links inside the
    document resolve correctly. Serving the index directly at /docs breaks relative hrefs. Doing
    what nginx does (redirect when the path lacks the trailing slash, serve when it has one) is the
    least surprising, but simply serving the index for both is defensible for a development server and
    a lot simpler.
  • Nothing else changes. Dot segments are already handled: HTTPDecoder.standardizePath removes
    them before any handler runs, which was the fix for Security vulnerability: Path traversal in DirectoryHTTPHandler #24. So this request is only about the index filename, not about traversal.

Context

From vdhamer/Photo-Club-Hub-HTML#249, a macOS app that generates a static site and previews it over
HTTP. The app currently ships its own handler that resolves the path and then delegates to
FileHTTPHandler for everything else. Two things keep it from being DirectoryHTTPHandler: the
directory index above, and a containment check that resolves symlinks — the latter genuinely belongs
in the app, since only the app knows which root a symlink has to stay under, so it is not part of
this request.


Note: this suggestion came out of adopting FlyingFox in
vdhamer/Photo-Club-Hub-HTML. I explicitly asked
Claude Code (Opus 5) for changes that would reduce the glue code on our side and would plausibly
benefit other FlyingFox users as well — the analysis above is its work against the 0.27.1
sources (yes, I did review it before filing). Offered as one user's suggestion; feel free to reject it if it does
not fit where you want the library to go.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions