Improve HTTP headers logic (#1584)

* Relax forbidden headers constraints
  - remove restriction on browser-related headers
- allow any glob pattern (no need to end with `/` or `*`, because glob
patterns already require users to explicitly declare prefix matches if
that's the intention)
* Replace `List<Pair<, ...>>`; use `Map<String, ...>` instead
* Use glob pattern strings as an API throughout, instead of `Pattern`
(e.g. in `HttpClientBuilder`)
* Add HTTP headers to message passing API
* Add HTTP headers to executor API (introduces `ExecutorSpiOptions4`)
* Add tests for Gradle, CLI, and pkl-executor invocations
* Improve documentation
* Add `isGlobPattern` API to class `String` for in-language validation
of http headers
* Behavior change: make sure explicitly configured `User-Agent` in
`HttpClientBuilder` can be shadowed by headers (allows users to set
`--http-header "**=User-Agent: My User Agent"` and for this to be the
only user agent).

CC @kyokuping
This commit is contained in:
Daniel Chao
2026-05-21 20:07:06 -07:00
committed by GitHub
parent 87ea28260b
commit 8e2e5e4ba8
48 changed files with 1067 additions and 222 deletions
+1
View File
@@ -47,6 +47,7 @@ dependencies {
}
testImplementation(projects.pklCommonsTest)
testImplementation(libs.wiremock)
}
sourceSets {
@@ -487,6 +487,7 @@ public class PklPlugin implements Plugin<Project> {
task.getTestPort().set(spec.getTestPort());
task.getHttpProxy().set(spec.getHttpProxy());
task.getHttpNoProxy().set(spec.getHttpNoProxy());
task.getHttpHeaders().set(spec.getHttpHeaders());
task.getHttpRewrites().set(spec.getHttpRewrites());
task.getExternalModuleReaders()
.set(providers.provider(() -> spec.getExternalModuleReaders().getAsMap()));
@@ -17,6 +17,8 @@ package org.pkl.gradle.spec;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
@@ -61,6 +63,8 @@ public interface BasePklSpec {
MapProperty<URI, URI> getHttpRewrites();
MapProperty<String, Map<String, List<String>>> getHttpHeaders();
NamedDomainObjectContainer<ExternalReaderSpec> getExternalModuleReaders();
NamedDomainObjectContainer<ExternalReaderSpec> getExternalResourceReaders();
@@ -25,6 +25,7 @@ import java.nio.file.Paths;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
@@ -50,7 +51,6 @@ import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.jspecify.annotations.Nullable;
import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.Pair;
import org.pkl.core.evaluatorSettings.Color;
import org.pkl.gradle.spec.ExternalReaderSpec;
import org.pkl.gradle.utils.PluginUtils;
@@ -167,7 +167,7 @@ public abstract class BasePklTask extends DefaultTask {
@Input
@Optional
public abstract ListProperty<Pair<Pattern, List<Pair<String, String>>>> getHttpHeaders();
public abstract MapProperty<String, Map<String, List<String>>> getHttpHeaders();
@Input
@Optional
@@ -15,6 +15,15 @@
*/
package org.pkl.gradle
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
import com.github.tomakehurst.wiremock.client.WireMock.ok
import com.github.tomakehurst.wiremock.client.WireMock.stubFor
import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
import com.github.tomakehurst.wiremock.client.WireMock.verify
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
import com.github.tomakehurst.wiremock.junit5.WireMockTest
import java.nio.file.Path
import kotlin.io.path.readText
import org.assertj.core.api.Assertions.assertThat
@@ -24,6 +33,7 @@ import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.readString
import org.pkl.commons.test.PackageServer
@WireMockTest
class EvaluatorsTest : AbstractTest() {
@Test
fun `render Pcf`() {
@@ -907,6 +917,28 @@ class EvaluatorsTest : AbstractTest() {
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
@Test
fun `http headers`(wwRuntimeInfo: WireMockRuntimeInfo) {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(ok("foo = 1")))
writeBuildFile(
"pcf",
"""
httpHeaders = [
"**": ["X-Foo": ["Foo"]]
]
allowedModules = ["http:", "pkl:", "repl:", "file:"]
"""
.trimIndent(),
)
writePklFile(
"""
res = import("${wwRuntimeInfo.httpBaseUrl}/foo.pkl")
"""
)
runTask("evalTest")
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("X-Foo", equalTo("Foo")))
}
private fun writeBuildFile(
// don't use `org.pkl.core.OutputFormat`
// because test compile class path doesn't contain pkl-core