Skip to content

can't load static file if path contains blank (%20) #3657

@LY1806620741

Description

@LY1806620741

Bug Description

Recently, I encountered an issue in my fresh project where static files fail to load if their path contains spaces – for example, accessing localhost:5173/image%20.png would not load the image. This issue went unnoticed initially, causing broken image rendering on my website without any obvious error cues.

I spent six hours troubleshooting this problem:

  1. Learned to debug Deno applications in VSCode
  2. Configured VSCode to debug Vite and fresh-vite-plugin
  3. Attempted to debug fresh-core, only to find it was evaluated via the vite-plugin (making direct debugging impossible)

Root Causes Identified

  1. plugin-vite not decode URI components using decodeURIComponent(), which blocks static resource loading at the primary layer (this appears to be the main mechanism for static file retrieval).
  2. The staticFile component imported via { staticFiles } from "fresh" (see staticFile) is effectively non-functional: it requires manual registration, yet the API is not publicly exposed – making it impossible to configure or inject custom logic.

Custom Solution Code

const MIME_TYPES: Record<string, string> = {
  txt: "text/plain",
  html: "text/html",
  css: "text/css",
  js: "application/javascript",
  json: "application/json",
  png: "image/png",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  gif: "image/gif",
  webp: "image/webp",
  ico: "image/x-icon",
  svg: "image/svg+xml",
  pdf: "application/pdf",
  mp4: "video/mp4",
};

app.use(async (ctx) => {
  const { req, url, config } = ctx;

  // Decode URI component to handle spaces in file paths
  let pathname = decodeURIComponent(url.pathname);
  
  // Normalize base path if configured
  if (config.basePath) {
    pathname = pathname !== config.basePath
      ? pathname.slice(config.basePath.length)
      : "/";
  }

  // 安全校验:防止路径遍历攻击(避免访问上级目录文件)Resolve to static directory
  pathname = `/static/${pathname}`;

  // Security: Prevent path traversal attacks (e.g., ../../../../)
  const safePath = path.resolve(Deno.cwd(), pathname.slice(1));
  const rootDir = path.resolve(Deno.cwd());

  if (!safePath.startsWith(rootDir)) {
    return new Response("Forbidden", { status: 403 });
  }

  // Check if file exists
  let fileInfo: Deno.FileInfo | undefined;
  try {
    fileInfo = await Deno.stat(safePath);
  } catch (_error) {
    return await ctx.next();
  }
  if (!fileInfo || !fileInfo.isFile) {
    return await ctx.next();
  }

  // Serve the file with proper headers
  try {
    const file = await Deno.open(safePath, { read: true });
    const ext = path.extname(safePath).toLowerCase().replace(".", "");
    const contentType = MIME_TYPES[ext];
    if (!contentType) {
      return await ctx.next();
    }

    const headers = new Headers({
      "Content-Type": contentType,
      "Content-Length": fileInfo.size.toString(),
      "Vary": "If-None-Match",
      "Last-Modified": fileInfo.mtime?.toUTCString() || new Date().toUTCString(),
    });

    return new Response(file.readable, { headers });
  } catch (_error) {
    return await ctx.next();
  }
});

version

$ deno -v
deno 2.6.0
$ deno task dev -v
Task dev vite "-v"
vite/7.3.0 linux-x64 node-v24.2.0
$ cat  deno.json |grep fresh
    "start": "deno serve -A _fresh/server.js",
    "update": "deno run -A -r jsr:@fresh/update ."
        "fresh",
    "**/_fresh/*"
    "fresh": "jsr:@fresh/core@^2.2.0",
    "@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.8",

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