Add support for HTTP rewrites (#1062)

This adds a new configuration option for the HTTP client to replace URI prefixes when making outbound calls.

Follows the design of https://github.com/apple/pkl-evolution/pull/17
This commit is contained in:
Daniel Chao
2025-07-16 15:53:31 -07:00
committed by GitHub
parent fea031a138
commit 99020bb79d
27 changed files with 607 additions and 47 deletions
@@ -1,3 +0,0 @@
import "pkl:analyze"
result = analyze.importGraph(Set("http://localhost:0/foo.pkl"))
@@ -1,14 +0,0 @@
–– Pkl Error ––
HTTP/1.1 header parser received no bytes
x | result = analyze.importGraph(Set("http://localhost:0/foo.pkl"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at analyzeInvalidHttpModule#result (file:///$snippetsDir/input/errors/analyzeInvalidHttpModule.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | bytes = text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,14 @@ import org.junit.jupiter.api.Test
class RequestRewritingClientTest {
private val captured = RequestCapturingClient()
private val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured)
private val client =
RequestRewritingClient(
"Pkl",
Duration.ofSeconds(42),
-1,
captured,
mapOf(URI("https://foo/") to URI("https://bar/")),
)
private val exampleUri = URI("https://example.com/foo/bar.html")
private val exampleRequest = HttpRequest.newBuilder(exampleUri).build()
@@ -114,7 +121,7 @@ class RequestRewritingClientTest {
@Test
fun `rewrites port 0 if test port is set`() {
val captured = RequestCapturingClient()
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured)
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured, mapOf())
val request = HttpRequest.newBuilder(URI("https://example.com:0")).build()
client.send(request, BodyHandlers.discarding())
@@ -130,4 +137,175 @@ class RequestRewritingClientTest {
assertThat(captured.request.uri().port).isEqualTo(0)
}
@Test
fun `matches rewrite rule`() {
fun assertThatRewriteMatches(uri: String, rule: String) =
assertThat(RequestRewritingClient.matchesRewriteRule(URI(uri), URI(rule)))
.`as`("$uri matches $rule")
assertThatRewriteMatches("https://www.foo.com/path/to/qux.html", "https://www.foo.com/").isTrue
assertThatRewriteMatches("HTTPS://www.foo.com/path/to/qux.html", "https://www.foo.com/").isTrue
assertThatRewriteMatches("HTTPS://WWW.FOO.COM/path/to/qux.html", "https://www.foo.com/").isTrue
assertThatRewriteMatches("https://www.foo.com/path/to/qux.html", "https://www.foo.com/path/")
.isTrue
assertThatRewriteMatches("https://www.foo.com/path/to/qux.html", "https://www.foo.com/PATH/")
.isFalse
assertThatRewriteMatches("https://www.foo.com", "https://www.foo.com/").isFalse
assertThatRewriteMatches(
"https://www.foo.com/path/to/qux.html?foo&bar",
"https://www.foo.com/path/to/qux.html?foo&bar",
)
.isTrue
assertThatRewriteMatches(
"https://www.foo.com/path/to/qux.html?foo&baz",
"https://www.foo.com/path/to/qux.html?foo&bar",
)
.isFalse
assertThatRewriteMatches(
"https://www.foo.com/path/to/qux.html?foo&bar#qux",
"https://www.foo.com/path/to/qux.html?foo&bar#q",
)
.isTrue
assertThatRewriteMatches(
"https://www.foo.com/path/to/qux.html?foo&bar#qux",
"https://www.foo.com/path/to/qux.html?foo&bar#w",
)
.isFalse
assertThatRewriteMatches(
"https://www.foo.com/path/to/qux.html?foo&bar",
"https://www.foo.com/path/to/qux.html?foo&bar#w",
)
.isFalse
assertThatRewriteMatches("https:///", "https:///").isTrue
assertThatRewriteMatches("https:///", "http:///").isFalse
// userinfo
assertThatRewriteMatches("https://foo@foo.com/", "http://foo.com/").isFalse
assertThatRewriteMatches("https://foo@foo.com/", "http://foo@foo.com/").isFalse
assertThatRewriteMatches("https://foo@foo.com/", "http://FOO@foo.com/").isFalse
}
@Test
fun `rewrites URIs`() {
assertThat(
rewrittenRequest(
"https://foo.com/bar/baz",
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
assertThat(
rewrittenRequest(
"https://FOO.COM/bar/baz",
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
assertThat(
rewrittenRequest(
"https://foo.com/bar/baz",
mapOf(URI("https://FOO.COM/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
assertThat(
rewrittenRequest(
"https://foo.com/bar/baz",
mapOf(URI("https://foo.com/") to URI("https://bar.com/qux/baz/")),
)
)
.isEqualTo("https://bar.com/qux/baz/bar/baz")
assertThat(
rewrittenRequest(
"https://foo.com/bar/baz",
mapOf(URI("https://foo.com/") to URI("https://bar.com/qux/baz/")),
)
)
.isEqualTo("https://bar.com/qux/baz/bar/baz")
assertThat(
rewrittenRequest(
"https://foo.com/bar/baz?qux=foo#corge",
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz?qux=foo#corge")
assertThat(
rewrittenRequest(
"https://fooey@foo.com/bar/baz",
mapOf(URI("https://fooey@foo.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
}
@Test
fun `rewrites URIs - longest rewrite wins`() {
assertThat(
rewrittenRequest(
"https://foo.com/qux/bar/baz",
mapOf(
URI("https://foo.com/") to URI("https://bar.com/"),
URI("https://foo.com/qux") to URI("https://corge.com/"),
),
)
)
.isEqualTo("https://corge.com/bar/baz")
}
@Test
fun `rewrites URIs - hostname is always lowercased`() {
assertThat(
rewrittenRequest(
"https://foo.com/bar/baz",
mapOf(URI("https://FOO.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
assertThat(
rewrittenRequest(
"https://FOO.com/bar/baz",
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
}
@Test
fun `rewrites URIs - scheme is always lowercased`() {
assertThat(
rewrittenRequest(
"HTTPS://foo.com/bar/baz",
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
assertThat(
rewrittenRequest(
"https://FOO.com/bar/baz",
mapOf(URI("HTTPS://foo.com/") to URI("HTTPS://bar.com/")),
)
)
.isEqualTo("https://bar.com/bar/baz")
}
private fun rewrittenRequest(uri: String, rules: Map<URI, URI>): String {
val captured = RequestCapturingClient()
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured, rules)
val request = HttpRequest.newBuilder(URI(uri)).build()
client.send(request, BodyHandlers.discarding())
return captured.request.uri().toString()
}
}
@@ -61,6 +61,9 @@ class PklSettingsTest {
"pkg.pkl-lang.org"
}
}
rewrites {
["https://foo.com/"] = "https://bar.com/"
}
}
"""
.trimIndent()
@@ -72,7 +75,8 @@ class PklSettingsTest {
PklEvaluatorSettings.Proxy(
URI("http://localhost:8080"),
listOf("example.com", "pkg.pkl-lang.org"),
)
),
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
)
assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp))
}
@@ -95,7 +99,10 @@ class PklSettingsTest {
val settings = PklSettings.loadFromPklHomeDir(tempDir)
val expectedHttp =
PklEvaluatorSettings.Http(PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()))
PklEvaluatorSettings.Http(
PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()),
null,
)
assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp))
}