Skip to content

[Bug]: read tool misdetects file MIME type — infer crate scans entire file content, causing source files with magic-byte literals to be sent as images/PDFs #3812

Description

@kassieclaire

Bug Description

The read tool's detect_mime_type function passes the entire file content to infer::get(). The infer crate scans the full buffer for magic bytes, not just the file header. When a source file (e.g., .ts, .py, .js) contains a magic-byte literal or comment (such as %PDF, ‰PNG, or \x89PNG), infer misidentifies the entire file as a binary document. The file is then base64-encoded and sent to the model as an image_url / application/pdf content part, causing provider errors and wasted tokens.

Steps to Reproduce

  1. Create a TypeScript file that contains a PDF magic-byte literal in a comment or array constant:

    // File: src/media/kind.ts
    import type { MediaKind } from "../domain/index.js";
    
    /**
     * Media kind detection by magic bytes
     */
    export const PDF_MAGIC = [0x25, 0x50, 0x44, 0x46]; // %PDF

    (The literal %PDF bytes appear at byte offset ~449 in the file.)

  2. Run forge with a model that supports vision (e.g., qwen3.8-max):

    forge -p "Read the file src/media/kind.ts and summarize it."
    
  3. The model receives the file as data:application/pdf;base64,aW1wb3J0IHR5cGU... instead of plain text. The base64 decodes to the TypeScript source.

  4. Provider returns an error such as:

    InternalError.Algo.InvalidParameter: The image format is illegal and cannot be opened.
    

Expected Behavior

detect_mime_type should only inspect the file header (first ~1 KB) when calling infer::get(). A .ts file containing a %PDF comment should be detected as text/plain (via extension fallback) and returned as text content, not base64-encoded as a PDF image.

Actual Behavior

infer::get() is called with the full file contents. Because kind.ts contains %PDF at byte 449, infer returns application/pdf. The file is then treated as a visual/binary document:

  • detect_mime_type returns "application/pdf"
  • is_visual_content returns true
  • The file is base64-encoded into a data:application/pdf;base64,... URL
  • The TypeScript source is sent to the model as a PDF "image"
  • The provider rejects it with InvalidParameter: The image format is illegal

Root Cause

In crates/forge_services/src/tool_services/fs_read.rs, the detect_mime_type function:

fn detect_mime_type(path: &Path, contents: &[u8]) -> &str {
    // infer::get() scans the ENTIRE contents buffer for magic bytes
    if let Some(file_type) = infer::get(contents) {
        return file_type.mime_type();
    }
    // ...extension fallback
}

Verified with a standalone test using infer 0.22.0:

Input to infer::get() Result
Full file (entire contents) application/pdf
First 100 bytes (header only) None → extension fallback → text/plain
First 450 bytes (before %PDF) Nonetext/plain
First 500 bytes (includes %PDF) application/pdf

Suggested Fix

Only pass the file header (first N bytes) to infer::get():

fn detect_mime_type(path: &Path, contents: &[u8]) -> &str {
    // Only inspect the file header — infer scans the full buffer for magic
    // bytes, so passing entire source files can trigger false positives when
    // the file contains magic-byte literals (e.g., a .ts file with a "%PDF"
    // comment).
    let header = &contents[..contents.len().min(1024)];
    if let Some(file_type) = infer::get(header) {
        return file_type.mime_type();
    }
    // ...extension fallback
}

Forge Version

forge 2.13.21 (commit 9b60797ef, built from source)

Operating System & Version

Ubuntu (Linux)

AI Provider

OpenAI-compatible (llmgateway routing to qwen3.8-max)

Model

qwen3.8-max

Installation Method

Built from source

Configuration

Reproduces with any provider/model that supports image input. The issue is in the read/fs_read tool's MIME detection, independent of provider config.

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