Skip to content

Web server cannot serve binary content (fonts, images) — response body and file reads are text-only #573

Description

@logbie

Summary

WFL's built-in web server cannot serve binary files (fonts, images, etc.) losslessly. Both the file-read path and the HTTP response body are String/UTF-8-based, so any non-UTF-8 bytes are corrupted before they reach the client. This blocks natural static-asset serving — e.g. self-hosting a .ttf font or serving a .png/.ico directly from a WFL web server.

Impact / motivation

Building the WFL website in WFL (serving its own marketing site via listen on port ...) requires serving a self-hosted Alegreya .ttf, a favicon, and potentially raster images. Today that's impossible without a workaround (base64 data: URIs embedded in a text CSS file), which inflates payloads and isn't how anyone expects static serving to work.

Root cause (two text-only chokepoints)

All references are in src/interpreter/mod.rs.

  1. File read is text-only. The WFL read content path uses read_to_string:

    // src/interpreter/mod.rs:739
    match AsyncReadExt::read_to_string(&mut file_clone, &mut contents).await {

    A .ttf/.png is corrupted the moment it is loaded. Note that binary-capable readers already exist internally but aren't wired to a WFL keyword or to the serve path:

    // src/interpreter/mod.rs:842
    async fn read_binary(&self, handle_id: &str) -> Result<Vec<u8>, String> { ... }
    // src/interpreter/mod.rs:869
    async fn read_binary_n(&self, handle_id: &str, count: usize) -> Result<Vec<u8>, String> { ... }
  2. The HTTP response body is a String. The response (and request) content is typed as String:

    // src/interpreter/mod.rs:75  (WflHttpRequest)
    pub body: String,
    // src/interpreter/mod.rs:82  (WflHttpResponse)
    pub content: String,

    So even a correctly-read Vec<u8> can't be carried through respond to req with <bytes> without a lossy String conversion.

Good news — the outbound warp path already accepts bytes

The warp reply is already built from bytes, so only the WFL-facing plumbing needs to change, not the HTTP layer:

// src/interpreter/mod.rs:5199 (approx)
match reply_builder.body(content_bytes) { ... }

Reproduction

  1. Write a WFL web server that reads a .ttf and responds with it:
    listen on port 8080 as server
    main loop:
        wait for request comes in on server as req
        open file at "public/assets/fonts/Alegreya-Variable.ttf" for reading as f
        store data as read content from f
        close file f
        respond to req with data and content_type "font/ttf"
    end loop
    
  2. curl -s localhost:8080/ -o out.ttf and compare with the source file.
  3. Expected: byte-identical font. Actual: size/bytes differ (UTF-8 lossy replacement) and the font fails to load in a browser.

Proposed fix

Keep bytes end-to-end from read → respond → warp:

  • Expose a binary read at the WFL level (e.g. read binary content from f) backed by the existing read_binary (mod.rs:842), yielding a bytes-capable value.
  • Allow respond to req with <bytes> ... to carry those bytes into warp's existing .body(content_bytes) (mod.rs:5199) without routing through the String content field — e.g. add a bytes variant to WflHttpResponse (content: Bytes | String) so text responses stay unchanged and binary responses stay lossless.
  • Bonus: a small built-in static-file/MIME helper so .ttf/.woff2/.png/.ico/.svg serve correctly out of the box (extension → content type, binary when appropriate).

Backward compatibility

Text responses (respond to req with "...") and text read content must keep working exactly as-is — the change is additive (a new binary read form + a bytes-capable response variant), preserving the backward-compatibility invariant.

Workaround until fixed

Base64-embed binary assets as data: URIs inside a text CSS file (which WFL serves fine as UTF-8). Works, but bloats payloads and defeats normal static serving.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions