Serve a static file with the right Content-Type

You’ll build: a tiny function that, given a request path rooted in a public/ directory, returns the bytes of the requested file along with a properly charset-annotated Content-Type header — exactly what a static-file middleware would do.

You’ll use: polycpp::mime::contentType(), polycpp::mime::lookup(), and polycpp::mime::charset() for the fallback case.

Prerequisites: installed and linking polycpp::mime. See Installation if not.

Step 1 — resolve the on-disk path

Before you touch MIME types you need a safe path. The library has no opinion about filesystem safety, but a typical handler rejects any path that escapes the document root:

#include <filesystem>

std::optional<std::filesystem::path>
resolveWithinRoot(const std::filesystem::path& root,
                  std::string_view urlPath) {
    auto full = std::filesystem::weakly_canonical(root / urlPath.substr(1));
    auto rel  = std::filesystem::relative(full, root);
    if (rel.empty() || rel.native().starts_with("..")) return std::nullopt;
    return full;
}

This is orthogonal to MIME handling, but most people trip over it first. Everything below assumes the request path is trusted.

Step 2 — pick a Content-Type

contentType takes either a MIME string or a bare extension. The filesystem path works because it treats anything without a / as an extension candidate:

#include <polycpp/mime/mime.hpp>

std::string pickContentType(const std::filesystem::path& file) {
    using namespace polycpp::mime;
    if (auto ct = contentType(file.extension().string())) {
        return *ct;
    }
    return "application/octet-stream";  // universal fallback
}

Two things to notice. First, we pass the extension with the leading dot (".html"); polycpp::mime::lookup() handles that form. Second, application/octet-stream is the defensible fallback when the extension is unknown — RFC 2046 §4.5.1 says so, and every browser treats it as “download, don’t render”.

Step 3 — decide whether to treat the body as text

After choosing the response Content-Type, you may still need to decide whether the bytes are safe to decode for logging, previews, or template processing. Let the charset helper make that call:

bool isTextual(std::string_view mimeType) {
    using namespace polycpp::mime;
    return charset(std::string(mimeType)).has_value();
}

polycpp::mime::charset() accepts a MIME type with optional parameters. It returns explicit mime-db charsets and the UTF-8 fallback for text/* types, so a separate text/ prefix check is redundant.

Step 4 — wire it together

#include <fstream>
#include <sstream>

struct StaticResponse {
    int         status;
    std::string contentType;
    std::string body;
};

StaticResponse serveStatic(const std::filesystem::path& root,
                           std::string_view urlPath) {
    auto file = resolveWithinRoot(root, urlPath);
    if (!file || !std::filesystem::is_regular_file(*file)) {
        return {404, "text/plain; charset=utf-8", "not found"};
    }

    std::ifstream in(*file, std::ios::binary);
    std::stringstream buf;
    buf << in.rdbuf();

    return {200, pickContentType(*file), buf.str()};
}

Drop that into whatever server loop you already have — polycpp’s http module, cpp-httplib, Crow, Boost.Beast, a raw socket — and the Content-Type column in the browser devtools Network pane should match what you’d see from Express or Nginx.

What you learned

  • contentType is the right entry point for response headers; it combines lookup and charset in one call.

  • application/octet-stream is the canonical fallback for unknown extensions — don’t invent your own.

  • The charset helper is the easiest way to tell “is this textual?” without hard-coding a list.

Next: Accept a vendor media type in a JSON API — using parse / format / test to accept a bespoke vendor media type in an API.