← All tasks
javascriptcodex/javascript-t1 #28Not a task: already works

Static HTTP File Server (javascript, written by Codex)

envgap__codex__javascript-t1-28

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #28 · read the task the agent was given
Codex wrote this javascript project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Static HTTP File Server

Write a program that serves static files over HTTP from a local directory, supporting directory listings, MIME type detection, caching headers, range requests for large files, and access logging.

FUNCTIONAL REQUIREMENTS:
- Accept a directory path to serve as a command-line argument (default: current directory)
- Start an HTTP server on a configurable port via --port flag (default: 8080) and bind address via --host flag (default: 0.0.0.0)
- Serve files with correct MIME types auto-detected from file extensions (HTML, CSS, JS, images, fonts, JSON, XML, PDF, video, audio, etc.)
- Generate directory listing pages when accessing a directory URL, showing file names, sizes, modification dates, and file type icons, with a --no-listing flag to disable this
- Support index files: automatically serve index.html or index.htm if present in a directory
- Implement HTTP caching headers: ETag based on file content hash, Last-Modified from file timestamp, and Cache-Control with configurable max-age via --cache flag (default: 3600 seconds)
- Support HTTP Range requests for partial content delivery (needed for video/audio streaming and resumable downloads)
- Log all requests to console in Common Log Format (CLF): remote address, timestamp, method, path, status code, response size, and response time
- Save access logs to a file via --log flag
- Support CORS headers via --cors flag to enable cross-origin requests (configurable allowed origins)
- Support HTTPS via --ssl flag with --cert and --key flags for certificate and private key file paths
- Print server startup information: URL, served directory, features enabled
- If no directory is given, create a sample directory with HTML, CSS, JS, image, and text files, then start serving them and print the URL to access each
- Handle errors: port already in use, permission denied, symlink traversal prevention, and graceful shutdown on SIGINT

Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "static-http-file-server",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "static-http-file-server",
      "version": "1.0.0",
      "dependencies": {},
      "engines": {
        "node": ">=20.0.0"
      }
    }
  }
}
package.json
{
  "name": "static-http-file-server",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}
README.md
# Static HTTP File Server (JavaScript)

Serves static files over HTTP/HTTPS with MIME detection, directory listing, cache headers, range support, CORS, and CLF access logs.

## Requirements
- Ubuntu 22.04
- Node.js 20+

## Dependencies
- No external runtime dependencies (Node standard library only)
- `package-lock.json` is included to pin the dependency graph (empty graph)

## Setup
```bash
npm install
```

## Run
Serve current directory:
```bash
npm start -- .
```

Custom host/port/cache:
```bash
npm start -- . --host 127.0.0.1 --port 9090 --cache 120
```

Disable listing and enable CORS:
```bash
npm start -- . --no-listing --cors "*"
```

Enable access log file:
```bash
npm start -- . --log access.log
```

Enable HTTPS:
```bash
npm start -- . --ssl --cert ./cert.pem --key ./key.pem
```

Demo mode (no directory argument): creates `sample_static_site` and serves it.
```bash
npm start
```

## Features
- Correct MIME types by extension
- Directory listings with names/sizes/modified dates/type indicators
- Index file support: `index.html`, `index.htm`
- Cache headers:
  - `ETag` (SHA-256 content hash)
  - `Last-Modified`
  - `Cache-Control` (`--cache`, default `3600`)
- HTTP Range requests (`206 Partial Content`)
- Console/file CLF-style access logs with response time
- CORS (`--cors <origin>`)
- HTTPS (`--ssl --cert --key`)
- Symlink traversal prevention and graceful `SIGINT` shutdown

## Example output
```text
Static HTTP File Server started
URL: http://0.0.0.0:8080
Serving directory: /home/user/project
Directory listing: enabled
Caching max-age: 3600
CORS: disabled
TLS: disabled
```
src/index.js
import crypto from "node:crypto";
import fs from "node:fs";
import http from "node:http";
import https from "node:https";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const MIME_TYPES = {
  ".html": "text/html; charset=utf-8",
  ".htm": "text/html; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "application/javascript; charset=utf-8",
  ".mjs": "application/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".xml": "application/xml; charset=utf-8",
  ".txt": "text/plain; charset=utf-8",
  ".md": "text/markdown; charset=utf-8",
  ".csv": "text/csv; charset=utf-8",
  ".pdf": "application/pdf",
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".gif": "image/gif",
  ".svg": "image/svg+xml",
  ".webp": "image/webp",
  ".ico": "image/x-icon",
  ".woff": "font/woff",
  ".woff2": "font/woff2",
  ".ttf": "font/ttf",
  ".otf": "font/otf",
  ".mp4": "video/mp4",
  ".webm": "video/webm",
  ".mp3": "audio/mpeg",
  ".wav": "audio/wav",
  ".ogg": "audio/ogg"
};

function parseArgs(argv) {
  const options = {
    host: "0.0.0.0",
    port: 8080,
    cache: 3600,
    noListing: false,
    cors: null,
    logPath: null,
    ssl: false,
    cert: null,
    key: null,
    directory: null
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (!arg.startsWith("--")) {
      if (!options.directory) options.directory = arg;
      continue;
    }
    if (arg === "--no-listing") {
      options.noListing = true;
      continue;
    }
    if (arg === "--ssl") {
      options.ssl = true;
      continue;
    }
    const key = arg.slice(2);
    if (i + 1 >= argv.length) throw new Error(`Missing value for ${arg}`);
    const value = argv[i + 1];
    i += 1;
    if (key === "host") options.host = value;
    else if (key === "port") options.port = Number.parseInt(value, 10);
    else if (key === "cache") options.cache = Number.parseInt(value, 10);
    else if (key === "cors") options.cors = value;
    else if (key === "log") options.logPath = value;
    else if (key === "cert") options.cert = value;
    else if (key === "key") options.key = value;
    else throw new Error(`Unknown option: ${arg}`);
  }

  if (!Number.isInteger(options.port) || options.port <= 0 || options.port > 65535) {
    throw new Error("Invalid --port. Expected 1..65535.");
  }
  if (!Number.isInteger(options.cache) || options.cache < 0) {
    throw new Error("Invalid --cache. Expected integer >= 0.");
  }
  if (options.ssl && (!options.cert || !options.key)) {
    throw new Error("When --ssl is enabled, both --cert and --key are required.");
  }
  return options;
}

function ensureDir(dirPath) {
  fs.mkdirSync(dirPath, { recursive: true });
}

function createSampleDirectory(baseDir) {
  ensureDir(baseDir);
  const cssDir = path.join(baseDir, "assets");
  const jsDir = path.join(baseDir, "scripts");
  ensureDir(cssDir);
  ensureDir(jsDir);

  fs.writeFileSync(
    path.join(baseDir, "index.html"),
    `<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Sample Static Server</title>
    <link rel="stylesheet" href="/assets/style.css" />
  </head>
  <body>
    <h1>Sample Static Server</h1>
    <p>If you can read this page, static serving works.</p>
    <img src="/sample.svg" alt="sample image" width="180" />
    <p><a href="/hello.txt">Open text file</a></p>
    <script src="/scripts/app.js"></script>
  </body>
</html>
`,
    "utf8"
  );
  fs.writeFileSync(
    path.join(cssDir, "style.css"),
    "body { font-family: sans-serif; margin: 2rem; } h1 { color: #1a5d8f; }\n",
    "utf8"
  );
  fs.writeFileSync(path.join(jsDir, "app.js"), "console.log('Sample JS loaded');\n", "utf8");
  fs.writeFileSync(path.join(baseDir, "hello.txt"), "Hello from sample static directory.\n", "utf8");
  fs.writeFileSync(
    path.join(baseDir, "sample.svg"),
    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 120">
  <rect width="320" height="120" fill="#e9f3fb"/>
  <circle cx="60" cy="60" r="32" fill="#1a5d8f"/>
  <text x="110" y="68" font-size="22" fill="#1a5d8f">Static Server</text>
</svg>
`,
    "utf8"
  );
}

function getMimeType(filePath) {
  return MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
}

function iconFor(entryName, isDir) {
  if (isDir) return "[DIR]";
  const ext = path.extname(entryName).toLowerCase();
  if ([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"].includes(ext)) return "[IMG]";
  if ([".mp4", ".webm", ".mp3", ".wav", ".ogg"].includes(ext)) return "[MED]";
  if ([".html", ".htm", ".css", ".js", ".json", ".xml", ".txt", ".md"].includes(ext)) return "[TXT]";
  return "[FIL]";
}

function htmlEscape(value) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function formatSize(size) {
  const units = ["B", "KB", "MB", "GB"];
  let value = size;
  let i = 0;
  while (value >= 1024 && i < units.length - 1) {
    value /= 1024;
    i += 1;
  }
  return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}

function formatDate(d) {
  return d.toISOString().replace("T", " ").replace("Z", " UTC");
}

function clfTimestamp(date) {
  const day = String(date.getDate()).padStart(2, "0");
  const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  const month = months[date.getMonth()];
  const year = date.getFullYear();
  const hh = String(date.getHours()).padStart(2, "0");
  const mm = String(date.getMinutes()).padStart(2, "0");
  const ss = String(date.getSeconds()).padStart(2, "0");
  const tzOffset = -date.getTimezoneOffset();
  const sign = tzOffset >= 0 ? "+" : "-";
  const tzAbs = Math.abs(tzOffset);
  const tzH = String(Math.floor(tzAbs / 60)).padStart(2, "0");
  const tzM = String(tzAbs % 60).padStart(2, "0");
  return `${day}/${month}/${year}:${hh}:${mm}:${ss} ${sign}${tzH}${tzM}`;
}

function computeEtag(filePath) {
  const data = fs.readFileSync(filePath);
  return `"${crypto.createHash("sha256").update(data).digest("hex")}"`;
}

function normalizeClientIp(value) {
  if (!value) return "-";
  return value.startsWith("::ffff:") ? value.slice(7) : value;
}

function sendError(res, statusCode, message, corsOrigin) {
  if (corsOrigin) {
    res.setHeader("Access-Control-Allow-Origin", corsOrigin);
    res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "*");
  }
  res.statusCode = statusCode;
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
  const body = `${statusCode} ${message}\n`;
  res.setHeader("Content-Length", Buffer.byteLength(body));
  res.end(body);
}

function directoryListingHtml(requestPath, directoryPath) {
  const entries = fs.readdirSync(directoryPath, { withFileTypes: true }).sort((a, b) => {
    if (a.isDirectory() && !b.isDirectory()) return -1;
    if (!a.isDirectory() && b.isDirectory()) return 1;
    return a.name.localeCompare(b.name);
  });
  const rows = [];
  if (requestPath !== "/") {
    const parent = path.posix.dirname(requestPath.replace(/\/$/, "")) || "/";
    rows.push(
      `<tr><td>[UP]</td><td><a href="${htmlEscape(parent.endsWith("/") ? parent : `${parent}/`)}">..</a></td><td>-</td><td>-</td></tr>`
    );
  }
  for (const entry of entries) {
    const fullPath = path.join(directoryPath, entry.name);
    const stat = fs.statSync(fullPath);
    const slash = entry.isDirectory() ? "/" : "";
    const href = path.posix.join(requestPath, entry.name).replace(/\\/g, "/") + slash;
    rows.push(
      `<tr><td>${iconFor(entry.name, entry.isDirectory())}</td><td><a href="${htmlEscape(href)}">${htmlEscape(
        entry.name + slash
      )}</a></td><td>${entry.isDirectory() ? "-" : formatSize(stat.size)}</td><td>${formatDate(stat.mtime)}</td></tr>`
    );
  }

  return `<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Index of ${htmlEscape(requestPath)}</title>
    <style>
      body { font-family: sans-serif; margin: 1.2rem; }
      table { border-collapse: collapse; width: 100%; }
      td, th { border-bottom: 1px solid #ddd; padding: 0.5rem; text-align: left; }
    </style>
  </head>
  <body>
    <h1>Index of ${htmlEscape(requestPath)}</h1>
    <table>
      <thead><tr><th>Type</th><th>Name</th><th>Size</th><th>Modified</th></tr></thead>
      <tbody>${rows.join("")}</tbody>
    </table>
  </body>
</html>`;
}

function parseRange(rangeHeader, totalSize) {
  const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader ?? "");
  if (!match) return null;
  let start = match[1] === "" ? null : Number.parseInt(match[1], 10);
  let end = match[2] === "" ? null : Number.parseInt(match[2], 10);
  if ((start !== null && Number.isNaN(start)) || (end !== null && Number.isNaN(end))) return null;
  if (start === null && end === null) return null;
  if (start === null) {
    const length = end;
    if (length <= 0) return null;
    start = Math.max(0, totalSize - length);
    end = totalSize - 1;
  } else if (end === null || end >= totalSize) {
    end = totalSize - 1;
  }
  if (start < 0 || end < start || start >= totalSize) return null;
  return { start, end };
}

function createLogger(logPath) {
  const stream = logPath ? fs.createWriteStream(logPath, { flags: "a" }) : null;
  return {
    write(line) {
      process.stdout.write(`${line}\n`);
      if (stream) stream.write(`${line}\n`);
    },
    close() {
      if (stream) stream.end();
    }
  };
}

function safeResolve(rootReal, requestPath) {
  const decoded = decodeURIComponent(requestPath);
  const resolved = path.resolve(rootReal, `.${decoded}`);
  if (resolved === rootReal || resolved.startsWith(`${rootReal}${path.sep}`)) {
    return resolved;
  }
  throw new Error("Path traversal blocked.");
}

function startServer(options) {
  let rootDir = options.directory;
  const cwd = process.cwd();
  if (!rootDir) {
    rootDir = path.join(cwd, "sample_static_site");
    createSampleDirectory(rootDir);
  }

  if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
    throw new Error(`Directory does not exist or is not a directory: ${rootDir}`);
  }

  const rootReal = fs.realpathSync(rootDir);
  const logger = createLogger(options.logPath);

  const requestHandler = (req, res) => {
    const started = process.hrtime.bigint();
    let statusCode = 500;
    let responseSize = 0;
    const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
    const corsOrigin = options.cors;

    const finishLog = () => {
      const elapsedMs = Number(process.hrtime.bigint() - started) / 1_000_000;
      const ip = normalizeClientIp(req.socket?.remoteAddress);
      const line = `${ip} - - [${clfTimestamp(new Date())}] "${req.method || "-"} ${parsedUrl.pathname} HTTP/${req.httpVersion}" ${statusCode} ${responseSize} ${elapsedMs.toFixed(2)}ms`;
      logger.write(line);
    };

    try {
      if (corsOrigin) {
        res.setHeader("Access-Control-Allow-Origin", corsOrigin);
        res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS");
        res.setHeader("Access-Control-Allow-Headers", "*");
      }
      if (req.method === "OPTIONS") {
        statusCode = 204;
        res.statusCode = 204;
        res.end();
        finishLog();
        return;
      }
      if (req.method !== "GET" && req.method !== "HEAD") {
        statusCode = 405;
        sendError(res, 405, "Method Not Allowed", corsOrigin);
        responseSize = 23;
        finishLog();
        return;
      }

      let resolvedPath = safeResolve(rootReal, parsedUrl.pathname);
      if (!fs.existsSync(resolvedPath)) {
        statusCode = 404;
        sendError(res, 404, "Not Found", corsOrigin);
        responseSize = 14;
        finishLog();
        return;
      }

      const entryReal = fs.realpathSync(resolvedPath);
      if (!(entryReal === rootReal || entryReal.startsWith(`${rootReal}${path.sep}`))) {
        statusCode = 403;
        sendError(res, 403, "Forbidden", corsOrigin);
        responseSize = 14;
        finishLog();
        return;
      }
      resolvedPath = entryReal;
      let stat = fs.statSync(resolvedPath);

      if (stat.isDirectory()) {
        const indexHtml = path.join(resolvedPath, "index.html");
        const indexHtm = path.join(resolvedPath, "index.htm");
        if (fs.existsSync(indexHtml) && fs.statSync(indexHtml).isFile()) {
          resolvedPath = indexHtml;
          stat = fs.statSync(resolvedPath);
        } else if (fs.existsSync(indexHtm) && fs.statSync(indexHtm).isFile()) {
          resolvedPath = indexHtm;
          stat = fs.statSync(resolvedPath);
        } else {
          if (options.noListing) {
            statusCode = 403;
            sendError(res, 403, "Directory Listing Disabled", corsOrigin);
            responseSize = 31;
            finishLog();
            return;
          }
          const withSlash = parsedUrl.pathname.endsWith("/") ? parsedUrl.pathname : `${parsedUrl.pathname}/`;
          const html = directoryListingHtml(withSlash, resolvedPath);
          const bodyBytes = Buffer.byteLength(html);
          statusCode = 200;
          responseSize = req.method === "HEAD" ? 0 : bodyBytes;
          res.statusCode = 200;
          res.setHeader("Content-Type", "text/html; charset=utf-8");
          res.setHeader("Content-Length", bodyBytes);
          if (req.method === "HEAD") res.end();
          else res.end(html);
          finishLog();
          return;
        }
      }

      if (!stat.isFile()) {
        statusCode = 403;
        sendError(res, 403, "Forbidden", corsOrigin);
        responseSize = 14;
        finishLog();
        return;
      }

      const etag = computeEtag(resolvedPath);
      const lastModified = stat.mtime.toUTCString();
      res.setHeader("ETag", etag);
      res.setHeader("Last-Modified", lastModified);
      res.setHeader("Cache-Control", `public, max-age=${options.cache}`);
      res.setHeader("Accept-Ranges", "bytes");
      res.setHeader("Content-Type", getMimeType(resolvedPath));

      const ifNoneMatch = req.headers["if-none-match"];
      const ifModifiedSince = req.headers["if-modified-since"];
      if (
        (typeof ifNoneMatch === "string" && ifNoneMatch === etag) ||
        (typeof ifModifiedSince === "string" && new Date(ifModifiedSince) >= stat.mtime)
      ) {
        statusCode = 304;
        res.statusCode = 304;
        res.end();
        finishLog();
        return;
      }

      const range = parseRange(typeof req.headers.range === "string" ? req.headers.range : null, stat.size);
      if (req.headers.range && !range) {
        statusCode = 416;
        res.statusCode = 416;
        res.setHeader("Content-Range", `bytes */${stat.size}`);
        res.end();
        finishLog();
        return;
      }

      let start = 0;
      let end = stat.size - 1;
      if (range) {
        start = range.start;
        end = range.end;
        statusCode = 206;
        res.statusCode = 206;
        res.setHeader("Content-Range", `bytes ${start}-${end}/${stat.size}`);
      } else {
        statusCode = 200;
        res.statusCode = 200;
      }
      const size = end - start + 1;
      responseSize = req.method === "HEAD" ? 0 : size;
      res.setHeader("Content-Length", size);

      if (req.method === "HEAD") {
        res.end();
        finishLog();
        return;
      }
      const stream = fs.createReadStream(resolvedPath, { start, end });
      stream.on("error", () => {
        if (!res.headersSent) sendError(res, 500, "Internal Server Error", corsOrigin);
      });
      res.on("finish", finishLog);
      stream.pipe(res);
    } catch (error) {
      statusCode = statusCode === 500 ? 500 : statusCode;
      responseSize = 0;
      sendError(res, 500, "Internal Server Error", corsOrigin);
      finishLog();
    }
  };

  let server;
  if (options.ssl) {
    const cert = fs.readFileSync(options.cert);
    const key = fs.readFileSync(options.key);
    server = https.createServer({ cert, key }, requestHandler);
  } else {
    server = http.createServer(requestHandler);
  }

  server.on("error", (err) => {
    if (err && err.code === "EADDRINUSE") {
      console.error(`Error: Port ${options.port} is already in use.`);
    } else if (err && err.code === "EACCES") {
      console.error(`Error: Permission denied on ${options.host}:${options.port}.`);
    } else {
      console.error(`Server error: ${err.message}`);
    }
    process.exitCode = 1;
  });

  server.listen(options.port, options.host, () => {
    const protocol = options.ssl ? "https" : "http";
    const url = `${protocol}://${options.host}:${options.port}`;
    console.log("Static HTTP File Server started");
    console.log(`URL: ${url}`);
    console.log(`Serving directory: ${rootReal}`);
    console.log(`Directory listing: ${options.noListing ? "disabled" : "enabled"}`);
    console.log(`Caching max-age: ${options.cache}`);
    console.log(`CORS: ${options.cors ? `enabled (${options.cors})` : "disabled"}`);
    console.log(`TLS: ${options.ssl ? "enabled" : "disabled"}`);
    if (!options.directory) {
      console.log(`Sample URLs: ${url}/ ${url}/assets/style.css ${url}/scripts/app.js ${url}/sample.svg ${url}/hello.txt`);
    }
  });

  process.on("SIGINT", () => {
    console.log("\nReceived SIGINT, shutting down gracefully...");
    server.close(() => {
      logger.close();
      process.exit(0);
    });
  });
}

function main() {
  try {
    const args = parseArgs(process.argv.slice(2));
    startServer(args);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
}

main();