Skip to content

feat: add HTTP URL rewrite support#46

Merged
jdx merged 2 commits intomainfrom
feat/http-rewrite
Mar 24, 2026
Merged

feat: add HTTP URL rewrite support#46
jdx merged 2 commits intomainfrom
feat/http-rewrite

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 24, 2026

Summary

  • Adds set_http_rewrites() to the Evaluator for configuring URL rewrite rules
  • Adds eval_to_json_with_options() with EvalOptions struct to the public API
  • Rewrite rules use source_prefix=target_prefix format matching pkl CLI's --http-rewrite
  • Longest-prefix-match semantics, applied in both fetch_source() and extract_package_zip()
  • This enables hk to support HK_PKL_HTTP_REWRITE with the pklr backend

Test plan

  • Added rewrite_url_longest_prefix_wins test
  • Added rewrite_url_no_rules_is_identity test
  • All 227 existing tests pass

This PR was generated by Claude Code.

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR adds HTTP URL rewrite support to pklr, mirroring the --http-rewrite flag of the pkl CLI. It introduces set_http_rewrites() on Evaluator, the public eval_to_json_with_options() entry point backed by an EvalOptions struct, and integrates longest-prefix rewriting into both fetch_source() and extract_package_zip().

  • The core rewrite logic (rewrite_url) is correct and the longest-prefix-match semantics match the pkl CLI's documented behavior.
  • EvalOptions and eval_to_json_with_options are well-structured; eval_to_json_with_client is cleanly delegated without breaking changes.
  • P1 – Empty source prefix matches all URLs: A rule like "=https://mirror.local/" (e.g., from a misconfigured env var) produces an empty source prefix which matches every URL via starts_with(""), silently redirecting all HTTP traffic to the target. No validation guards against this.
  • P2 – Silent rule discard: Malformed rules (missing =) are silently dropped by filter_map with no warning, making mis-configurations invisible to callers.
  • P2 – Error messages hide original URL: When a rewrite is applied, error messages report only the rewritten fetch_url, losing the original URL that was in the pkl source file and making failures harder to diagnose.
  • Test coverage for rewrite_url is good for happy-path cases but lacks edge-case tests (empty source, malformed rules, = in source).

Confidence Score: 3/5

  • Mergeable with caution — the empty-source-prefix footgun should be addressed before this is exposed through user-facing configuration like HK_PKL_HTTP_REWRITE.
  • The core logic is sound and the longest-prefix-match semantics are correctly implemented. However, the missing validation for empty source prefixes is a real footgun that can silently redirect all HTTP requests to an unintended host, especially when rules are sourced from environment variables. The silent discard of malformed rules compounds this — there is no feedback mechanism for mis-configurations.
  • src/eval.rs — specifically the set_http_rewrites parsing logic and error-message quality in fetch_source.

Important Files Changed

Filename Overview
src/eval.rs Adds set_http_rewrites, rewrite_url, and integrates rewriting into fetch_source / extract_package_zip. Core logic is correct; issues include: empty source prefix matching all URLs (P1), silent discarding of malformed rules (P2), and error messages hiding the original URL when rewrites are applied (P2).
src/lib.rs Introduces EvalOptions struct and eval_to_json_with_options top-level function. Clean design: existing eval_to_json_with_client delegates cleanly to the new options-based function, preserving backward compatibility.
tests/pkl_features.rs Two synchronous unit tests added for rewrite_url: longest-prefix-match and identity (no rules). Tests cover the happy path well but do not cover edge cases such as malformed rules, empty source prefix, or rules with = in the source.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant lib as lib.rs (eval_to_json_with_options)
    participant Evaluator
    participant rewrite as rewrite_url()
    participant http_cache
    participant HTTP as HTTP Server

    Caller->>lib: eval_to_json_with_options(path, EvalOptions { http_rewrites })
    lib->>Evaluator: new() + set_http_rewrites(rules)
    lib->>Evaluator: eval_source(source, path)

    Evaluator->>Evaluator: fetch_source(original_url)
    Evaluator->>rewrite: rewrite_url(original_url)
    rewrite-->>Evaluator: fetch_url (longest-prefix match applied)

    Evaluator->>http_cache: get(fetch_url)
    alt cache hit
        http_cache-->>Evaluator: cached body
    else cache miss
        Evaluator->>HTTP: GET fetch_url
        HTTP-->>Evaluator: response body
        Evaluator->>http_cache: insert(fetch_url, body)
    end

    Evaluator-->>lib: Value
    lib-->>Caller: serde_json::Value
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "[autofix.ci] apply automated fixes" | Re-trigger Greptile

Comment thread src/eval.rs
Comment on lines +64 to +72
pub fn set_http_rewrites(&mut self, rules: &[String]) {
self.http_rewrites = rules
.iter()
.filter_map(|rule| {
let (src, tgt) = rule.split_once('=')?;
Some((src.to_string(), tgt.to_string()))
})
.collect();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Empty source prefix silently matches all URLs

split_once('=') on a rule like "=https://mirror.local/" succeeds and produces ("", "https://mirror.local/"). Since url.starts_with("") is always true in Rust, this empty-source rule will match every URL and redirect all HTTP traffic to the target. There is no guard against an empty source prefix, and no error or warning is emitted.

If a user accidentally adds a leading = (e.g., from a misconfigured env var like HK_PKL_HTTP_REWRITE=https://mirror.local/), every fetch will silently be rerouted.

Consider validating the parsed rule before inserting it:

pub fn set_http_rewrites(&mut self, rules: &[String]) {
    self.http_rewrites = rules
        .iter()
        .filter_map(|rule| {
            let (src, tgt) = rule.split_once('=')?;
            if src.is_empty() {
                eprintln!("pklr: ignoring rewrite rule with empty source prefix: {rule}");
                return None;
            }
            Some((src.to_string(), tgt.to_string()))
        })
        .collect();
}

Fix in Claude Code

Comment thread src/eval.rs
Comment on lines +64 to +72
pub fn set_http_rewrites(&mut self, rules: &[String]) {
self.http_rewrites = rules
.iter()
.filter_map(|rule| {
let (src, tgt) = rule.split_once('=')?;
Some((src.to_string(), tgt.to_string()))
})
.collect();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Malformed rules silently dropped

Rules that do not contain = are silently discarded by filter_map. If a caller passes a mis-formatted rule string, they receive no indication that the rule was ignored. This could make debugging very difficult, especially when the feature is configured through an environment variable like HK_PKL_HTTP_REWRITE.

Since set_http_rewrites currently returns (), it cannot propagate errors. Consider either:

  1. Returning Result<()> and propagating the first malformed rule as an error, or
  2. At minimum, adding a debug!/eprintln! for any rule that fails to parse.

Fix in Claude Code

Comment thread src/eval.rs
Comment on lines +126 to 144
let fetch_url = rewritten.as_ref();
if let Some(cached) = self.http_cache.get(fetch_url) {
return Ok(cached.clone());
}
let body = self
.http_client
.get(url)
.get(fetch_url)
.send()
.await
.map_err(|e| Error::Eval(format!("HTTP fetch failed for {url}: {e}")))?
.map_err(|e| Error::Eval(format!("HTTP fetch failed for {fetch_url}: {e}")))?
.error_for_status()
.map_err(|e| Error::Eval(format!("HTTP error for {url}: {e}")))?
.map_err(|e| Error::Eval(format!("HTTP error for {fetch_url}: {e}")))?
.text()
.await
.map_err(|e| Error::Eval(format!("HTTP read failed for {url}: {e}")))?;
self.http_cache.insert(url.to_string(), body.clone());
.map_err(|e| Error::Eval(format!("HTTP read failed for {fetch_url}: {e}")))?;
self.http_cache.insert(fetch_url.to_string(), body.clone());
Ok(body)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Cache keyed by rewritten URL may obscure original URL in errors

The http_cache is now keyed by fetch_url (the post-rewrite URL) rather than the original url. While this is logically correct (two original URLs that rewrite to the same mirror will share a cache entry), the error messages now report only the rewritten URL, making it harder to correlate a failure back to the original URL the pkl file imported.

Consider including the original URL in error messages when a rewrite is active:

let orig = url;
let rewritten = self.rewrite_url(url);
let fetch_url = rewritten.as_ref();
// ...
.map_err(|e| {
    if fetch_url != orig {
        Error::Eval(format!("HTTP fetch failed for {orig} (rewritten to {fetch_url}): {e}"))
    } else {
        Error::Eval(format!("HTTP fetch failed for {fetch_url}: {e}"))
    }
})?

Fix in Claude Code

@jdx jdx force-pushed the feat/http-rewrite branch from 8835f3c to 3a88cb5 Compare March 24, 2026 01:33
Add `set_http_rewrites()` to the Evaluator and `eval_to_json_with_options()`
to the public API. Rewrite rules use the same `source_prefix=target_prefix`
format as pkl CLI's `--http-rewrite` flag, with longest-prefix-match
semantics.

Rewrites are applied in `fetch_source()` and `extract_package_zip()`,
covering all HTTP fetches (imports, amends, extends, packages, read()).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the feat/http-rewrite branch from 698ee46 to 4baa96a Compare March 24, 2026 01:36
@jdx jdx merged commit 6524d92 into main Mar 24, 2026
6 checks passed
@jdx jdx deleted the feat/http-rewrite branch March 24, 2026 01:41
@jdx jdx mentioned this pull request Mar 24, 2026
jdx added a commit that referenced this pull request Mar 24, 2026
## 🤖 New release

* `pklr`: 0.2.1 -> 0.2.2 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

## [0.2.2](v0.2.1...v0.2.2) -
2026-03-24

### Added

- add HTTP URL rewrite support
([#46](#46))

### Fixed

- strip inherited class definitions from extends and remote amends
([#48](#48))
- strip inherited class definitions from amends output
([#45](#45))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
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.

1 participant