Skip to content

feat: implement Accept header content negotiation for image formats#94

Open
twangodev wants to merge 4 commits intomainfrom
feat/header-accept
Open

feat: implement Accept header content negotiation for image formats#94
twangodev wants to merge 4 commits intomainfrom
feat/header-accept

Conversation

@twangodev
Copy link
Copy Markdown
Owner

@twangodev twangodev commented Dec 29, 2025

Summary by CodeRabbit

  • New Features

    • Added HTTP Accept header content negotiation for image format selection.
    • Supported formats: WebP, PNG, JPEG; best-match selection honors quality (q) values and browser-style headers.
    • Explicit ?format= query parameter overrides Accept negotiation.
    • Default remains PNG when no preference is detected.
  • Tests

    • Added unit tests covering Accept header scenarios and explicit-format overrides.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 29, 2025 21:16
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 29, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds Accept header content negotiation for image output formats (WebP, PNG, JPEG). Introduces src/accept.rs, two dependencies, integrates negotiation into parameter processing and the route handler, and updates render-options logic and tests.

Changes

Cohort / File(s) Summary
Dependency additions
Cargo.toml
Added headers-accept = "0.3.0" and mediatype = "0.21.0".
Accept negotiation module
src/accept.rs
New module implementing negotiate_format(accept_header: Option<&str>) -> Option<OutputFormat>; defines supported media types (image/webp, image/png, image/jpeg) and includes unit tests covering Accept scenarios.
Module registration
src/main.rs
Added mod accept;.
Parameter handling
src/params.rs
Replaced render_options() with render_options_with_accept(&self, accept_header: Option<&str>) -> RenderOptions; ?format= overrides Accept negotiation; defaults to PNG; tests updated.
Route handler
src/routes/index.rs
generate() now accepts HeaderMap; extracts Accept header and calls params.render_options_with_accept(accept_header); imports adjusted.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant Handler as HTTP Handler\n(src/routes/index.rs)
    participant Params as Params\n(src/params.rs)
    participant Accept as Negotiation\n(src/accept.rs)
    participant Renderer as Renderer/Output

    Client->>Handler: GET /... + Accept header
    Handler->>Handler: extract Accept header
    Handler->>Params: render_options_with_accept(accept_header)
    alt explicit ?format provided
        Params->>Renderer: use explicit format
    else negotiate via Accept
        Params->>Accept: negotiate_format(accept_header)
        Accept->>Accept: parse Accept, apply q-values
        Accept-->>Params: selected OutputFormat or None
        alt negotiated
            Params->>Renderer: use negotiated format
        else no match
            Params->>Renderer: default to PNG
        end
    end
    Renderer-->>Client: image response (selected format)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I sniff the headers, light and keen,
image/webp, png, jpeg in between.
I choose by q-value, polite and spry,
or follow a ?format if it's nigh.
Hop—served the image, neat and spry.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing Accept header content negotiation for image formats, which is reflected throughout the changeset including new dependencies, the accept.rs module, and integration into the request handling pipeline.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 29, 2025

Codecov Report

❌ Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/routes/index.rs 0.00% 2 Missing ⚠️
Flag Coverage Δ
rust 21.53% <86.66%> (+0.59%) ⬆️
typescript 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/accept.rs 100.00% <100.00%> (ø)
src/main.rs 0.00% <ø> (ø)
src/params.rs 32.43% <100.00%> (+1.00%) ⬆️
src/routes/index.rs 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements content negotiation for image formats using the Accept HTTP header according to RFC 9110. The implementation adds support for clients to request their preferred image format (WebP, PNG, or JPEG) through the Accept header while maintaining backward compatibility with explicit format query parameters.

Key Changes:

  • Adds content negotiation logic using the headers-accept crate to parse and negotiate Accept headers based on quality values
  • Implements a three-tier priority system: explicit ?format= parameter > Accept header negotiation > PNG default
  • Introduces comprehensive test coverage for the negotiation logic

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/accept.rs New module implementing RFC 9110 content negotiation for image formats with quality value parsing
src/params.rs Updates render_options to accept an Accept header parameter and implements priority-based format selection
src/routes/index.rs Extracts Accept header from incoming requests and passes it to the render options builder
src/main.rs Registers the new accept module
Cargo.toml Adds headers-accept and mediatype dependencies for content negotiation
Cargo.lock Lock file updates for new dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/accept.rs Outdated
Comment on lines +27 to +30
let available: Vec<mediatype::MediaType> = [IMAGE_WEBP, IMAGE_PNG, IMAGE_JPEG]
.iter()
.filter_map(|s| mediatype::MediaType::parse(s).ok())
.collect();
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The available formats vector is being created and allocated on every request. Since these media types are constant and the parsing is infallible for valid MIME types, this allocation could be moved to a static or lazy_static to improve performance. Consider using once_cell::sync::Lazy or similar to initialize this once at startup rather than on every request.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/params.rs (1)

113-136: Fix unsafe practice in tests: call validate() before render_options_with_accept().

The Accept header negotiation logic is well-designed. However, production code (src/routes/index.rs line 96-107) correctly calls validate() before render_options_with_accept(), but the test functions in src/params.rs do not. Four test functions call render_options_with_accept() without calling validate() first, violating the documented safety requirement:

  • test_render_options_defaults
  • test_render_options_custom
  • test_render_options_accept_header_negotiation
  • test_render_options_format_overrides_accept

While these tests don't panic because they use valid format values, they undermine the safety contract and could mask issues if test defaults change. Update these tests to call validate() before render_options_with_accept().

🧹 Nitpick comments (1)
src/accept.rs (1)

1-41: LGTM with optional optimization suggestion.

The implementation correctly follows RFC 9110 content negotiation semantics with proper error handling via Option returns. The logic flow is clean and well-documented.

💡 Optional: Consider caching the available media types vector

The available media types vector is reconstructed on every call. For a minor performance improvement, consider using LazyLock (stable in Rust 1.80+) to initialize it once:

+use std::sync::LazyLock;
+
+static AVAILABLE_FORMATS: LazyLock<Vec<mediatype::MediaType>> = LazyLock::new(|| {
+    [IMAGE_WEBP, IMAGE_PNG, IMAGE_JPEG]
+        .iter()
+        .filter_map(|s| mediatype::MediaType::parse(s).ok())
+        .collect()
+});
+
 pub fn negotiate_format(accept_header: Option<&str>) -> Option<OutputFormat> {
     let header = accept_header?;
     let accept = Accept::from_str(header).ok()?;
 
-    // Available formats we support
-    let available: Vec<mediatype::MediaType> = [IMAGE_WEBP, IMAGE_PNG, IMAGE_JPEG]
-        .iter()
-        .filter_map(|s| mediatype::MediaType::parse(s).ok())
-        .collect();
-
-    // Negotiate returns the best match based on client preferences (RFC 9110)
-    let best = accept.negotiate(&available)?;
+    let best = accept.negotiate(&AVAILABLE_FORMATS)?;

This is a micro-optimization and not critical for correctness.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39be525 and 1f02dc5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • Cargo.toml
  • src/accept.rs
  • src/main.rs
  • src/params.rs
  • src/routes/index.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use cargo fmt to format Rust code
Use cargo clippy to lint Rust code

Files:

  • src/routes/index.rs
  • src/accept.rs
  • src/params.rs
  • src/main.rs
🧠 Learnings (1)
📚 Learning: 2025-12-16T04:03:37.838Z
Learnt from: CR
Repo: twangodev/ogis PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T04:03:37.838Z
Learning: Applies to src/generator/png.rs : Default image dimensions are 1200x630 pixels

Applied to files:

  • src/params.rs
🧬 Code graph analysis (3)
src/routes/index.rs (1)
src/telemetry/config.rs (1)
  • headers (54-60)
src/accept.rs (3)
web/src/lib/stores/playground.svelte.ts (1)
  • OutputFormat (21-21)
src/generator/render.rs (1)
  • from_str (21-28)
src/config.rs (1)
  • parse (177-179)
src/params.rs (3)
src/accept.rs (1)
  • negotiate_format (22-41)
web/src/lib/stores/playground.svelte.ts (1)
  • OutputFormat (21-21)
src/generator/render.rs (1)
  • from_str (21-28)
🪛 GitHub Check: Format Check
src/params.rs

[warning] 122-122:
Diff in /home/runner/work/ogis/ogis/src/params.rs

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: Code Coverage
  • GitHub Check: Code Coverage
🔇 Additional comments (7)
src/main.rs (1)

1-1: LGTM!

The module declaration correctly integrates the new Accept header negotiation module into the crate.

src/params.rs (2)

6-6: LGTM!

The import correctly references the new negotiate_format function from the accept module.


296-311: LGTM!

The test coverage for Accept header negotiation is comprehensive, verifying both the negotiation behavior and the priority of explicit format parameters over Accept headers.

src/accept.rs (1)

43-106: LGTM!

Excellent test coverage including edge cases (nil/empty headers), single format matches, quality precedence scenarios, unsupported formats, and realistic browser-like Accept headers. The tests validate RFC 9110 quality-based negotiation correctly.

src/routes/index.rs (2)

11-11: LGTM!

The addition of HeaderMap to imports and the handler signature change correctly enables Accept header extraction from incoming requests.

Also applies to: 65-65


105-107: LGTM!

The Accept header extraction and integration with render_options_with_accept is clean and idiomatic. The code correctly:

  • Extracts the Accept header using header::ACCEPT constant
  • Converts to Option<&str> with .ok()
  • Passes it to the negotiation logic
  • Maintains the safety requirement by calling validate() at line 96 before render_options_with_accept
Cargo.toml (1)

14-14: Dependencies are current and free from known vulnerabilities.

Both headers-accept (0.3.0) and mediatype (0.21.0) are pinned to their latest available versions and have no known security advisories in the RustSec database. No action needed.

Copilot AI review requested due to automatic review settings December 29, 2025 22:18
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01100bf and a0ad7a0.

📒 Files selected for processing (1)
  • src/accept.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use cargo fmt to format Rust code
Use cargo clippy to lint Rust code

Files:

  • src/accept.rs
🪛 GitHub Check: Format Check
src/accept.rs

[warning] 7-7:
Diff in /home/runner/work/ogis/ogis/src/accept.rs

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Agent
  • GitHub Check: Code Coverage
  • GitHub Check: Code Coverage
  • GitHub Check: build
🔇 Additional comments (3)
src/accept.rs (3)

17-37: LGTM! Clean implementation of content negotiation.

The function correctly implements RFC 9110 quality value negotiation:

  • Handles all error cases gracefully by returning None
  • Uses idiomatic Rust with the ? operator for early returns
  • Delegates negotiation logic to the headers-accept crate
  • The linear search through SUPPORTED_FORMATS is appropriate for 3 elements

The implementation is clear, well-documented, and follows Rust best practices.


39-102: Excellent test coverage!

The test suite comprehensively covers:

  • Missing and empty headers
  • Individual format matching
  • Quality value precedence (RFC 9110 compliance)
  • Unsupported format handling
  • Real-world browser Accept headers

The browser-like header test (lines 95-101) is particularly valuable for ensuring compatibility with actual client requests.


12-15: LazyLock optimization is excellent and the .unwrap() is safe.

The static initialization with LazyLock properly ensures MIME types are parsed once at startup rather than on every request. The .unwrap() on line 14 is safe because all OutputFormat variants are hardcoded to return valid MIME strings: "image/png", "image/jpeg", and "image/webp". Since the enum only has these three controlled variants, there is no path for invalid MIME data, and this is covered by test cases.

Comment thread src/accept.rs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/params.rs
Comment on lines +294 to +309
#[test]
fn test_render_options_accept_header_negotiation() {
let params = default_params();
// Accept header should be used when no format param
let opts = params.render_options_with_accept(Some("image/webp"));
assert_eq!(opts.format, OutputFormat::WebP);
}

#[test]
fn test_render_options_format_overrides_accept() {
let mut params = default_params();
params.format = Some("jpeg".to_string());
// Explicit format should override Accept header
let opts = params.render_options_with_accept(Some("image/webp"));
assert_eq!(opts.format, OutputFormat::Jpeg);
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests should call validate() before render_options_with_accept() to properly demonstrate the expected usage pattern documented in the function's docstring (line 120). While the tests currently pass because they use valid formats, they don't reflect the actual usage flow where validation must occur first to prevent panics from the expect() call on line 124.

Consider adding assert!(params.validate(1000).is_ok()); before calling render_options_with_accept() in both test cases to align with the documented contract and actual production usage in the route handler.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/accept.rs (1)

14-16: Consider using expect() for clearer panic messages.

The .unwrap() on line 15 will panic if content_type() returns an invalid MIME type. While this should never happen given that OutputFormat::content_type() is under your control, using .expect() with a descriptive message would make debugging easier if this invariant is ever violated in the future.

🔎 Optional improvement
 static AVAILABLE_MIMES: LazyLock<[mediatype::MediaType; 3]> = LazyLock::new(|| {
-    SUPPORTED_FORMATS.map(|f| mediatype::MediaType::parse(f.content_type()).unwrap())
+    SUPPORTED_FORMATS.map(|f| {
+        mediatype::MediaType::parse(f.content_type())
+            .expect("OutputFormat::content_type() must return valid MIME types")
+    })
 });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0ad7a0 and 9f6d2de.

📒 Files selected for processing (1)
  • src/accept.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use cargo fmt to format Rust code
Use cargo clippy to lint Rust code

Files:

  • src/accept.rs
🧬 Code graph analysis (1)
src/accept.rs (2)
web/src/lib/stores/playground.svelte.ts (1)
  • OutputFormat (21-21)
src/generator/render.rs (1)
  • from_str (21-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Code Coverage
  • GitHub Check: Code Coverage
🔇 Additional comments (4)
src/accept.rs (4)

1-7: LGTM: Clean module structure.

The module documentation and imports are well-organized. The use of LazyLock from std::sync is appropriate for one-time initialization of the parsed MIME types.


18-38: LGTM: Clean negotiation logic.

The negotiation function is well-implemented:

  • Proper error handling with early returns via ? operator
  • Uses .essence() to correctly strip MIME type parameters before comparison
  • Linear search through 3 formats is appropriate and efficient

40-103: LGTM: Comprehensive test coverage.

The unit tests are well-structured and cover all important scenarios:

  • Missing and empty Accept headers
  • Individual format matching for all three supported types
  • Quality value precedence (both explicit and implicit q=1.0)
  • Unsupported formats
  • Real-world browser Accept headers

The test coverage ensures the negotiation logic is robust and handles edge cases correctly.


3-3: No action required—headers-accept is already at the latest stable version.

The headers-accept crate version 0.3.0 is already the current latest stable version available on crates.io. No security advisories exist for this crate in the RustSec database.

Copilot AI review requested due to automatic review settings February 19, 2026 08:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/accept.rs
Comment on lines +40 to +103
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_no_accept_header() {
assert_eq!(negotiate_format(None), None);
}

#[test]
fn test_empty_accept_header() {
assert_eq!(negotiate_format(Some("")), None);
}

#[test]
fn test_webp_only() {
assert_eq!(
negotiate_format(Some("image/webp")),
Some(OutputFormat::WebP)
);
}

#[test]
fn test_png_only() {
assert_eq!(negotiate_format(Some("image/png")), Some(OutputFormat::Png));
}

#[test]
fn test_jpeg_only() {
assert_eq!(
negotiate_format(Some("image/jpeg")),
Some(OutputFormat::Jpeg)
);
}

#[test]
fn test_quality_preference_png_wins() {
// PNG has higher q value than WebP
let result = negotiate_format(Some("image/webp;q=0.8, image/png;q=0.9"));
assert_eq!(result, Some(OutputFormat::Png));
}

#[test]
fn test_quality_preference_webp_wins() {
// WebP has implicit q=1.0, PNG has q=0.9
let result = negotiate_format(Some("image/png;q=0.9, image/webp"));
assert_eq!(result, Some(OutputFormat::WebP));
}

#[test]
fn test_unsupported_format_returns_none() {
assert_eq!(negotiate_format(Some("image/gif")), None);
assert_eq!(negotiate_format(Some("text/html")), None);
}

#[test]
fn test_browser_like_header() {
// Chrome-like Accept header for images
let header = "image/avif,image/webp,image/apng,image/svg+xml,image/*;q=0.8,*/*;q=0.5";
let result = negotiate_format(Some(header));
// Should return WebP since we don't support avif/apng/svg
assert_eq!(result, Some(OutputFormat::WebP));
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case for when Accept header specifies wildcard image/* or */* alone (without specific formats). This would verify that the negotiation properly selects a supported format when only wildcards are provided. For example:

#[test]
fn test_wildcard_image_only() {
    assert_eq!(
        negotiate_format(Some("image/*")),
        Some(OutputFormat::WebP) // or whichever format the library chooses first
    );
}

#[test]
fn test_wildcard_all_only() {
    assert_eq!(
        negotiate_format(Some("*/*")),
        Some(OutputFormat::WebP) // or whichever format the library chooses first
    );
}

This ensures the behavior is well-defined and documented when browsers send generic Accept headers.

Copilot uses AI. Check for mistakes.
Comment thread src/routes/index.rs

// Get render options and record in span
let render_options = params.render_options();
// Get render options with Accept header negotiation
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a code comment here explaining the Accept header negotiation behavior for future maintainers. For example:

// Extract Accept header for content negotiation
// Supports: image/webp, image/png, image/jpeg with quality values (q)
// Falls back to PNG if no match or header absent

This makes the feature more discoverable when reading the code.

Suggested change
// Get render options with Accept header negotiation
// Extract Accept header for content negotiation.
// Supports: image/webp, image/png, image/jpeg with quality values (q).
// Falls back to PNG if no match is found or the header is absent.

Copilot uses AI. Check for mistakes.
Comment thread src/accept.rs

use crate::generator::OutputFormat;

/// All supported output formats for negotiation.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining why the formats are ordered [WebP, Png, Jpeg]. While the order doesn't directly affect negotiation (quality values take precedence), it may influence the behavior when the headers-accept library has to break ties. A comment like:

/// All supported output formats for negotiation.
/// Order may influence tie-breaking when Accept header has equal quality values.
const SUPPORTED_FORMATS: [OutputFormat; 3] =
    [OutputFormat::WebP, OutputFormat::Png, OutputFormat::Jpeg];

This makes the ordering decision explicit and documents potential edge case behavior.

Suggested change
/// All supported output formats for negotiation.
/// All supported output formats for negotiation.
/// Order may influence tie-breaking when Accept header has equal quality values.

Copilot uses AI. Check for mistakes.
Comment thread src/params.rs
Comment on lines +294 to 310
#[test]
fn test_render_options_accept_header_negotiation() {
let params = default_params();
// Accept header should be used when no format param
let opts = params.render_options_with_accept(Some("image/webp"));
assert_eq!(opts.format, OutputFormat::WebP);
}

#[test]
fn test_render_options_format_overrides_accept() {
let mut params = default_params();
params.format = Some("jpeg".to_string());
// Explicit format should override Accept header
let opts = params.render_options_with_accept(Some("image/webp"));
assert_eq!(opts.format, OutputFormat::Jpeg);
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case for when the Accept header contains an unsupported format but no explicit format parameter is provided. This verifies the fallback behavior. For example:

#[test]
fn test_render_options_unsupported_accept_fallback() {
    let params = default_params();
    // Should fall back to PNG when Accept header has no supported format
    let opts = params.render_options_with_accept(Some("image/gif"));
    assert_eq!(opts.format, OutputFormat::Png);
}

This ensures the documented behavior (Priority 3: Default to PNG) is tested.

Copilot uses AI. Check for mistakes.
Comment thread src/accept.rs

/// Parsed MIME types for negotiation, initialized once.
static AVAILABLE_MIMES: LazyLock<[mediatype::MediaType; 3]> = LazyLock::new(|| {
SUPPORTED_FORMATS.map(|f| mediatype::MediaType::parse(f.content_type()).unwrap())
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() call here could be made more explicit about why it's safe. While it will never fail (since the hardcoded MIME types are valid), consider adding a comment explaining this invariant or using .expect() with a descriptive message. This improves code maintainability by making the safety assumption clear to future readers.

Suggested change
SUPPORTED_FORMATS.map(|f| mediatype::MediaType::parse(f.content_type()).unwrap())
SUPPORTED_FORMATS.map(|f| {
mediatype::MediaType::parse(f.content_type())
.expect("OutputFormat::content_type() must return a valid MIME type")
})

Copilot uses AI. Check for mistakes.
Comment thread src/params.rs
Comment on lines +123 to +124
OutputFormat::from_str(fmt)
.expect("format should be validated before calling render_options_with_accept")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .expect() call creates a brittle API contract that relies on runtime validation. While the current code correctly calls validate() before this method, this design makes the API error-prone for future changes. Consider one of these alternatives:

  1. Make the validation part of this method by returning Result<RenderOptions, ApiError> and validating the format here if present
  2. Accept OutputFormat directly instead of Option<String> for the format field (though this would require changes to deserialization)
  3. Use a fallback to the default format instead of panicking: OutputFormat::from_str(fmt).unwrap_or_default()

Option 3 would be the simplest and maintain backward compatibility while being more defensive.

Suggested change
OutputFormat::from_str(fmt)
.expect("format should be validated before calling render_options_with_accept")
OutputFormat::from_str(fmt).unwrap_or_default()

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants