From 57066435fcb58ef3dfe624108873b0d29e0d6500 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sun, 31 Aug 2025 23:08:34 +0900 Subject: [PATCH 01/20] Add support for customizing HTTP headers --- .../org/pkl/commons/cli/CliBaseOptions.kt | 3 +++ .../kotlin/org/pkl/commons/cli/CliCommand.kt | 5 +++++ .../pkl/commons/cli/commands/BaseOptions.kt | 9 +++++++++ .../PklEvaluatorSettings.java | 20 ++++++++++++------- .../java/org/pkl/core/http/HttpClient.java | 8 ++++++++ .../org/pkl/core/http/HttpClientBuilder.java | 10 +++++++++- .../java/org/pkl/core/http/JdkHttpClient.java | 12 ++++++++--- .../org/pkl/core/settings/PklSettingsTest.kt | 5 +++++ .../java/org/pkl/gradle/task/BasePklTask.java | 5 +++++ .../java/org/pkl/gradle/task/ModulesTask.java | 1 + stdlib/EvaluatorSettings.pkl | 3 +++ 11 files changed, 70 insertions(+), 11 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt index b3d6b314c..527180ede 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt @@ -144,6 +144,9 @@ data class CliBaseOptions( /** URL prefixes to rewrite. */ val httpRewrites: Map? = null, + /** HTTP headers to add to the request. */ + val httpHeaders: Map? = null, + /** External module reader process specs */ val externalModuleReaders: Map = mapOf(), diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt index a59da40a3..52c5392fb 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt @@ -218,6 +218,10 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { cliOptions.httpRewrites ?: evaluatorSettings?.http?.rewrites ?: settings.http?.rewrites() } + private val httpHeaders: Map? by lazy { + cliOptions.httpHeaders ?: project?.evaluatorSettings?.http?.headers ?: settings.http?.headers + } + protected val externalModuleReaders: Map by lazy { (evaluatorSettings?.externalModuleReaders ?: emptyMap()) + cliOptions.externalModuleReaders } @@ -277,6 +281,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { setProxy(proxyAddress, noProxy ?: listOf()) } httpRewrites?.let(::setRewrites) + httpHeaders?.let(::setHeaders) // Lazy building significantly reduces execution time of commands that do minimal work. // However, it means that HTTP client initialization errors won't surface until an HTTP // request is made. diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index bb17c918f..cbb980c32 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -285,6 +285,14 @@ class BaseOptions : OptionGroup() { .multiple() .toMap() + val httpHeaders: Map by + option( + names = arrayOf("--http-headers"), + metavar = "key=value", + help = "HTTP header to add to the request.", + ) + .associate() + val externalModuleReaders: Map by option( names = arrayOf("--external-module-reader"), @@ -351,6 +359,7 @@ class BaseOptions : OptionGroup() { httpProxy = proxy, httpNoProxy = noProxy, httpRewrites = httpRewrites.ifEmpty { null }, + httpHeaders = httpHeaders.ifEmpty { null }, externalModuleReaders = externalModuleReaders, externalResourceReaders = externalResourceReaders, traceMode = traceMode, diff --git a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java index bcb196a91..4bf290045 100644 --- a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java @@ -126,8 +126,11 @@ public static PklEvaluatorSettings parse( traceMode == null ? null : TraceMode.valueOf(traceMode.toUpperCase())); } - public record Http(@Nullable Proxy proxy, @Nullable Map rewrites) { - public static final Http DEFAULT = new Http(null, Collections.emptyMap()); + public record Http( + @Nullable Proxy proxy, + @Nullable Map rewrites, + @Nullable Map headers) { + public static final Http DEFAULT = new Http(null, Collections.emptyMap(), null); @SuppressWarnings("unchecked") public static @Nullable Http parse(@Nullable Value input) { @@ -136,10 +139,9 @@ public record Http(@Nullable Proxy proxy, @Nullable Map rewrites) { } else if (input instanceof PObject http) { var proxy = Proxy.parse((Value) http.getProperty("proxy")); var rewrites = http.getProperty("rewrites"); - if (rewrites instanceof PNull) { - return new Http(proxy, null); - } else { - var parsedRewrites = new HashMap(); + HashMap parsedRewrites = null; + if (!(rewrites instanceof PNull)) { + parsedRewrites = new HashMap(); for (var entry : ((Map) rewrites).entrySet()) { var key = entry.getKey(); var value = entry.getValue(); @@ -149,8 +151,12 @@ public record Http(@Nullable Proxy proxy, @Nullable Map rewrites) { throw new PklException(ErrorMessages.create("invalidUri", e.getInput())); } } - return new Http(proxy, parsedRewrites); } + var headers = http.getProperty("headers"); + return new Http( + proxy, + parsedRewrites, + headers instanceof PNull ? null : ((Map) headers)); } else { throw PklBugException.unreachableCode(); } diff --git a/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java index c0d9c4bd1..d7a2d54f1 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java @@ -150,6 +150,14 @@ interface Builder { */ Builder addRewrite(URI sourcePrefix, URI targetPrefix); + /** + * Sets the HTTP headers for the request, replacing any previously configured headers. + * + *

This method clears all existing headers and replaces them with the contents of the + * provided map. + */ + Builder setHeaders(Map headers); + /** * Creates a new {@code HttpClient} from the current state of this builder. * diff --git a/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java b/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java index 980ca2e20..b50aa669d 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java @@ -39,6 +39,7 @@ final class HttpClientBuilder implements HttpClient.Builder { private int testPort = -1; private ProxySelector proxySelector; private Map rewrites = new HashMap<>(); + private Map headers = new HashMap<>(); HttpClientBuilder() { var release = Release.current(); @@ -110,6 +111,12 @@ public Builder addRewrite(URI sourcePrefix, URI targetPrefix) { return this; } + @Override + public Builder setHeaders(Map headers) { + this.headers = new HashMap<>(headers); + return this; + } + @Override public HttpClient build() { return doBuild().get(); @@ -127,7 +134,8 @@ private Supplier doBuild() { this.proxySelector != null ? this.proxySelector : java.net.ProxySelector.getDefault(); return () -> { var jdkClient = - new JdkHttpClient(certificateFiles, certificateBytes, connectTimeout, proxySelector); + new JdkHttpClient( + certificateFiles, certificateBytes, connectTimeout, proxySelector, headers); return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient, rewrites); }; } diff --git a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java index 0758830c2..b639cd2bb 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java @@ -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. @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import javax.annotation.concurrent.ThreadSafe; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; @@ -54,6 +55,7 @@ final class JdkHttpClient implements HttpClient { // non-private for testing final java.net.http.HttpClient underlying; + final Map headers; // call java.net.http.HttpClient.close() if available (JDK 21+) private static final MethodHandle closeMethod; @@ -77,7 +79,8 @@ final class JdkHttpClient implements HttpClient { List certificateFiles, List certificateBytes, Duration connectTimeout, - java.net.ProxySelector proxySelector) { + java.net.ProxySelector proxySelector, + Map headers) { underlying = java.net.http.HttpClient.newBuilder() .sslContext(createSslContext(certificateFiles, certificateBytes)) @@ -85,13 +88,16 @@ final class JdkHttpClient implements HttpClient { .proxy(proxySelector) .followRedirects(Redirect.NORMAL) .build(); + this.headers = headers; } @Override public HttpResponse send(HttpRequest request, BodyHandler responseBodyHandler) throws IOException { try { - return underlying.send(request, responseBodyHandler); + var wrappedRequestBuilder = HttpRequest.newBuilder(request, (name, value) -> true); + this.headers.forEach(wrappedRequestBuilder::header); + return underlying.send(wrappedRequestBuilder.build(), responseBodyHandler); } catch (ConnectException e) { // original exception has no message throw new ConnectException( diff --git a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt index a82fc3b22..f7b9c8c35 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt @@ -64,6 +64,9 @@ class PklSettingsTest { rewrites { ["https://foo.com/"] = "https://bar.com/" } + headers { + ["X-Foo"] = "bar" + } } """ .trimIndent() @@ -77,6 +80,7 @@ class PklSettingsTest { listOf("example.com", "pkg.pkl-lang.org"), ), mapOf(URI("https://foo.com/") to URI("https://bar.com/")), + mapOf("X-Foo" to "bar"), ) assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp)) } @@ -102,6 +106,7 @@ class PklSettingsTest { PklEvaluatorSettings.Http( PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()), null, + null, ) assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp)) } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java index bfd3a4b68..e7b4c9f2e 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java @@ -161,6 +161,10 @@ public Provider getEvalRootDirPath() { @Optional public abstract MapProperty getHttpRewrites(); + @Input + @Optional + public abstract MapProperty getHttpHeaders(); + @Input @Optional public abstract Property getPowerAssertions(); @@ -218,6 +222,7 @@ protected CliBaseOptions getCliBaseOptions() { getHttpProxy().getOrNull(), getHttpNoProxy().getOrElse(List.of()), getHttpRewrites().getOrNull(), + getHttpHeaders().getOrNull(), Map.of(), Map.of(), null, diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java index 4f6a0347b..d32c23c88 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java @@ -164,6 +164,7 @@ protected CliBaseOptions getCliBaseOptions() { null, List.of(), getHttpRewrites().getOrNull(), + getHttpHeaders().getOrNull(), Map.of(), Map.of(), null, diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index d79b95e9d..4a1058eec 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -169,6 +169,9 @@ class Http { /// (not schematically enforced). @Since { version = "0.29.0" } rewrites: Mapping? + + /// HTTP headers to add to every request. + headers: Mapping? } /// Settings that control how Pkl talks to HTTP proxies. From e119cab93a41c5059ce090441c5118771490f212 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sat, 6 Sep 2025 23:41:55 +0900 Subject: [PATCH 02/20] Allow customizing headers for different URLs --- .../org/pkl/commons/cli/CliBaseOptions.kt | 3 +- .../kotlin/org/pkl/commons/cli/CliCommand.kt | 2 +- .../pkl/commons/cli/commands/BaseOptions.kt | 43 +++++++++++++++++-- .../PklEvaluatorSettings.java | 24 ++++++++--- .../java/org/pkl/core/http/HttpClient.java | 3 +- .../org/pkl/core/http/HttpClientBuilder.java | 7 +-- .../java/org/pkl/core/http/JdkHttpClient.java | 14 ++++-- .../org/pkl/core/settings/PklSettingsTest.kt | 8 +++- .../java/org/pkl/gradle/task/BasePklTask.java | 3 +- stdlib/EvaluatorSettings.pkl | 4 +- 10 files changed, 88 insertions(+), 23 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt index 527180ede..26f9cae6f 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt @@ -20,6 +20,7 @@ import java.nio.file.Files import java.nio.file.Path import java.time.Duration import java.util.regex.Pattern +import org.pkl.core.Pair import org.pkl.core.evaluatorSettings.Color import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.TraceMode @@ -145,7 +146,7 @@ data class CliBaseOptions( val httpRewrites: Map? = null, /** HTTP headers to add to the request. */ - val httpHeaders: Map? = null, + val httpHeaders: Map>>? = null, /** External module reader process specs */ val externalModuleReaders: Map = mapOf(), diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt index 52c5392fb..9b44fb622 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt @@ -218,7 +218,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { cliOptions.httpRewrites ?: evaluatorSettings?.http?.rewrites ?: settings.http?.rewrites() } - private val httpHeaders: Map? by lazy { + private val httpHeaders: Map>>? by lazy { cliOptions.httpHeaders ?: project?.evaluatorSettings?.http?.headers ?: settings.http?.headers } diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index cbb980c32..4edefc4da 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -31,6 +31,7 @@ import java.util.regex.Pattern import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliException import org.pkl.commons.shlex +import org.pkl.core.Pair as PPair import org.pkl.core.evaluatorSettings.Color import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.TraceMode @@ -285,13 +286,49 @@ class BaseOptions : OptionGroup() { .multiple() .toMap() - val httpHeaders: Map by + val httpHeaders: Map>> by option( names = arrayOf("--http-headers"), - metavar = "key=value", + metavar = "=

:
[,
:
...]", help = "HTTP header to add to the request.", ) - .associate() + .convert { it -> + val (uriStr, headers) = + it.split("=", limit = 2).let { parts -> + require(parts.size == 2) { + "Headers must be in the form of =
:
" + } + parts[0] to parts[1] + } + + try { + val uri = URI(uriStr.trim()) + + val headerPairs = + headers.split(',').map { header -> + val headerParts = header.split(":", limit = 2) + require(headerParts.size == 2) { "Header '$header' is not in 'name:value' format. " } + PPair(headerParts[0], headerParts[1]) + } + uri to headerPairs + } catch (e: IllegalArgumentException) { + fail(e.message!!) + } catch (e: URISyntaxException) { + val message = buildString { + append("HTTP headers target `${e.input}` has invalid syntax (${e.reason}).") + if (e.index > -1) { + append("\n\n") + append(e.input) + append("\n") + append(" ".repeat(e.index)) + append("^") + } + } + fail(message) + } + } + .multiple() + .toMap() val externalModuleReaders: Map by option( diff --git a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java index 4bf290045..2be94e74d 100644 --- a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java @@ -30,6 +30,7 @@ import org.pkl.core.Duration; import org.pkl.core.PNull; import org.pkl.core.PObject; +import org.pkl.core.Pair; import org.pkl.core.PklBugException; import org.pkl.core.PklException; import org.pkl.core.Value; @@ -129,7 +130,7 @@ public static PklEvaluatorSettings parse( public record Http( @Nullable Proxy proxy, @Nullable Map rewrites, - @Nullable Map headers) { + @Nullable Map>> headers) { public static final Http DEFAULT = new Http(null, Collections.emptyMap(), null); @SuppressWarnings("unchecked") @@ -141,7 +142,7 @@ public record Http( var rewrites = http.getProperty("rewrites"); HashMap parsedRewrites = null; if (!(rewrites instanceof PNull)) { - parsedRewrites = new HashMap(); + parsedRewrites = new HashMap<>(); for (var entry : ((Map) rewrites).entrySet()) { var key = entry.getKey(); var value = entry.getValue(); @@ -153,10 +154,21 @@ public record Http( } } var headers = http.getProperty("headers"); - return new Http( - proxy, - parsedRewrites, - headers instanceof PNull ? null : ((Map) headers)); + HashMap>> parsedHeaders = null; + if (!(headers instanceof PNull)) { + parsedHeaders = new HashMap<>(); + var headersMap = (Map>>) headers; + for (var entry : headersMap.entrySet()) { + var key = entry.getKey(); + var value = entry.getValue(); + try { + parsedHeaders.put(new URI(key), value); + } catch (URISyntaxException e) { + throw new PklException(ErrorMessages.create("invalidUri", e.getInput())); + } + } + } + return new Http(proxy, parsedRewrites, parsedHeaders); } else { throw PklBugException.unreachableCode(); } diff --git a/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java index d7a2d54f1..41d1faa91 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import javax.net.ssl.SSLContext; +import org.pkl.core.Pair; import org.pkl.core.util.Nullable; /** @@ -156,7 +157,7 @@ interface Builder { *

This method clears all existing headers and replaces them with the contents of the * provided map. */ - Builder setHeaders(Map headers); + Builder setHeaders(Map>> headers); /** * Creates a new {@code HttpClient} from the current state of this builder. diff --git a/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java b/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java index b50aa669d..3935f02a7 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.function.Supplier; +import org.pkl.core.Pair; import org.pkl.core.Release; import org.pkl.core.http.HttpClient.Builder; @@ -39,7 +40,7 @@ final class HttpClientBuilder implements HttpClient.Builder { private int testPort = -1; private ProxySelector proxySelector; private Map rewrites = new HashMap<>(); - private Map headers = new HashMap<>(); + private Map>> headers = new HashMap<>(); HttpClientBuilder() { var release = Release.current(); @@ -112,8 +113,8 @@ public Builder addRewrite(URI sourcePrefix, URI targetPrefix) { } @Override - public Builder setHeaders(Map headers) { - this.headers = new HashMap<>(headers); + public Builder setHeaders(Map>> headers) { + this.headers = headers; return this; } diff --git a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java index b639cd2bb..e4c0ee871 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java @@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.ConnectException; +import java.net.URI; import java.net.http.HttpClient.Redirect; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -47,6 +48,7 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManagerFactory; +import org.pkl.core.Pair; import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.Exceptions; @@ -55,7 +57,7 @@ final class JdkHttpClient implements HttpClient { // non-private for testing final java.net.http.HttpClient underlying; - final Map headers; + final Map>> headers; // call java.net.http.HttpClient.close() if available (JDK 21+) private static final MethodHandle closeMethod; @@ -80,7 +82,7 @@ final class JdkHttpClient implements HttpClient { List certificateBytes, Duration connectTimeout, java.net.ProxySelector proxySelector, - Map headers) { + Map>> headers) { underlying = java.net.http.HttpClient.newBuilder() .sslContext(createSslContext(certificateFiles, certificateBytes)) @@ -96,7 +98,13 @@ public HttpResponse send(HttpRequest request, BodyHandler responseBody throws IOException { try { var wrappedRequestBuilder = HttpRequest.newBuilder(request, (name, value) -> true); - this.headers.forEach(wrappedRequestBuilder::header); + for (var entry : headers.entrySet()) { + if (RequestRewritingClient.matchesRewriteRule(request.uri(), entry.getKey())) { + for (var value : entry.getValue()) { + wrappedRequestBuilder.header(value.getFirst(), value.getSecond()); + } + } + } return underlying.send(wrappedRequestBuilder.build(), responseBodyHandler); } catch (ConnectException e) { // original exception has no message diff --git a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt index f7b9c8c35..28072b986 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt @@ -26,6 +26,7 @@ import org.pkl.commons.writeString import org.pkl.core.Evaluator import org.pkl.core.ModuleSource import org.pkl.core.PObject +import org.pkl.core.Pair as PPair import org.pkl.core.evaluatorSettings.PklEvaluatorSettings import org.pkl.core.settings.PklSettings.Editor @@ -65,7 +66,9 @@ class PklSettingsTest { ["https://foo.com/"] = "https://bar.com/" } headers { - ["X-Foo"] = "bar" + ["https://foo.com/"] { + Pair("X-Foo", "bar") + } } } """ @@ -80,8 +83,9 @@ class PklSettingsTest { listOf("example.com", "pkg.pkl-lang.org"), ), mapOf(URI("https://foo.com/") to URI("https://bar.com/")), - mapOf("X-Foo" to "bar"), + mapOf(URI("https://foo.com/") to listOf(PPair("X-Foo", "bar"))), ) + assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp)) } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java index e7b4c9f2e..3e337d9d1 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java @@ -48,6 +48,7 @@ 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.utils.PluginUtils; @@ -163,7 +164,7 @@ public Provider getEvalRootDirPath() { @Input @Optional - public abstract MapProperty getHttpHeaders(); + public abstract MapProperty>> getHttpHeaders(); @Input @Optional diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 4a1058eec..58c9279ef 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -170,8 +170,8 @@ class Http { @Since { version = "0.29.0" } rewrites: Mapping? - /// HTTP headers to add to every request. - headers: Mapping? + /// HTTP headers to add to outbound requests targeting specified URLs. + headers: Mapping>>? } /// Settings that control how Pkl talks to HTTP proxies. From 5e62b61f86cd3c0c0b5473811d790c1476fcb3ed Mon Sep 17 00:00:00 2001 From: JaeEun Kim <109906379+kyokuping@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:47:23 +0900 Subject: [PATCH 03/20] add `@Since` annotation in pkl option Co-authored-by: Jen Basch --- stdlib/EvaluatorSettings.pkl | 1 + 1 file changed, 1 insertion(+) diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 58c9279ef..6c72318b3 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -171,6 +171,7 @@ class Http { rewrites: Mapping? /// HTTP headers to add to outbound requests targeting specified URLs. + @Since { version = "0.30.0" } headers: Mapping>>? } From d48b858cf1cadc4dd2aa957ab3443e790ac1e720 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 19 Sep 2025 15:32:33 +0900 Subject: [PATCH 04/20] validate `header` syntax --- .../pkl/commons/cli/commands/BaseOptions.kt | 15 +++++++++++--- .../PklEvaluatorSettings.java | 20 ++++++++++++++++--- .../org/pkl/core/errorMessages.properties | 6 ++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index 4edefc4da..9b24b2305 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -33,6 +33,7 @@ import org.pkl.commons.cli.CliException import org.pkl.commons.shlex import org.pkl.core.Pair as PPair import org.pkl.core.evaluatorSettings.Color +import org.pkl.core.evaluatorSettings.PklEvaluatorSettings import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.TraceMode import org.pkl.core.runtime.VmUtils @@ -304,11 +305,19 @@ class BaseOptions : OptionGroup() { try { val uri = URI(uriStr.trim()) + val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") val headerPairs = headers.split(',').map { header -> - val headerParts = header.split(":", limit = 2) - require(headerParts.size == 2) { "Header '$header' is not in 'name:value' format. " } - PPair(headerParts[0], headerParts[1]) + val (headerName, headerValue) = + headerRegex.find(header)?.destructured + ?: fail("Header '$header' is not in 'name:value' format.") + require(PklEvaluatorSettings.HEADER_NAME_REGEX.matcher(headerName).matches()) { + "HTTP header name '$headerName' has invalid syntax." + } + require(PklEvaluatorSettings.HEADER_VALUE_REGEX.matcher(headerValue).matches()) { + "HTTP header value '$headerValue' has invalid syntax" + } + PPair(headerName, headerValue) } uri to headerPairs } catch (e: IllegalArgumentException) { diff --git a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java index 2be94e74d..19eef79d7 100644 --- a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java @@ -54,6 +54,10 @@ public record PklEvaluatorSettings( @Nullable Map externalResourceReaders, @Nullable TraceMode traceMode) { + public static final Pattern HEADER_NAME_REGEX = Pattern.compile("^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$"); + public static final Pattern HEADER_VALUE_REGEX = + Pattern.compile("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$"); + /** Initializes a {@link PklEvaluatorSettings} from a raw object representation. */ @SuppressWarnings("unchecked") public static PklEvaluatorSettings parse( @@ -159,10 +163,20 @@ public record Http( parsedHeaders = new HashMap<>(); var headersMap = (Map>>) headers; for (var entry : headersMap.entrySet()) { - var key = entry.getKey(); - var value = entry.getValue(); + var uri = entry.getKey(); + var pairs = entry.getValue(); + for (var pair : pairs) { + if (!HEADER_NAME_REGEX.matcher(pair.getFirst()).matches()) { + throw new PklException( + ErrorMessages.create("invalidHeaderName", pair.getFirst())); + } + if (!HEADER_VALUE_REGEX.matcher(pair.getSecond()).matches()) { + throw new PklException( + ErrorMessages.create("invalidHeaderValue", pair.getSecond())); + } + } try { - parsedHeaders.put(new URI(key), value); + parsedHeaders.put(new URI(uri), pairs); } catch (URISyntaxException e) { throw new PklException(ErrorMessages.create("invalidUri", e.getInput())); } diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index e9a4147ea..f66d2f852 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -1124,3 +1124,9 @@ Option {1}s must not overlap with built-in options. commandFlagInvalidType=\ Option `{0}` with annotation `@{1}` has invalid type `{2}`.\n\ Expected type: `{3}` + +invalidHeaderName=\ +HTTP header name `{0}` has invalid syntax. + +invalidHeaderValue=\ +HTTP header value `{0}` has invalid syntax. From 26fff36fef3d78403c24a9d4b52d462521054222 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 14 Nov 2025 00:36:49 +0900 Subject: [PATCH 05/20] Rename HttpRewrite to HttpPrefix for clarity --- stdlib/EvaluatorSettings.pkl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 6c72318b3..893449970 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -126,7 +126,10 @@ local const hasNonEmptyHostname = (it: String) -> /// A key or value in [Http.rewrites]. @Since { version = "0.29.0" } -typealias HttpRewrite = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname) +typealias HttpPrefix = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname) + +@Deprecated { since = "0.30.0"; replaceWith = "HttpPrefix" } +typealias HttpRewrite = HttpPrefix /// Settings that control how Pkl talks to HTTP(S) servers. class Http { @@ -168,11 +171,11 @@ class Http { /// An rewrite target should also not contain a query string or fragment component /// (not schematically enforced). @Since { version = "0.29.0" } - rewrites: Mapping? + rewrites: Mapping? /// HTTP headers to add to outbound requests targeting specified URLs. @Since { version = "0.30.0" } - headers: Mapping>>? + headers: Mapping>>? } /// Settings that control how Pkl talks to HTTP proxies. From ce945a3c5b5a295ebb549f29643b51773fc8963e Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 14 Nov 2025 00:37:11 +0900 Subject: [PATCH 06/20] Add strict HTTP header validation to EvaluatorSettings --- .../PklEvaluatorSettings.java | 7 ++- stdlib/EvaluatorSettings.pkl | 54 ++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java index 19eef79d7..9ba201450 100644 --- a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java @@ -167,13 +167,12 @@ public record Http( var pairs = entry.getValue(); for (var pair : pairs) { if (!HEADER_NAME_REGEX.matcher(pair.getFirst()).matches()) { - throw new PklException( - ErrorMessages.create("invalidHeaderName", pair.getFirst())); + throw new PklException(ErrorMessages.create("invalidHeaderName", pair.getFirst())); } if (!HEADER_VALUE_REGEX.matcher(pair.getSecond()).matches()) { throw new PklException( - ErrorMessages.create("invalidHeaderValue", pair.getSecond())); - } + ErrorMessages.create("invalidHeaderValue", pair.getSecond())); + } } try { parsedHeaders.put(new URI(uri), pairs); diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 893449970..cfaabaf36 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -175,7 +175,7 @@ class Http { /// HTTP headers to add to outbound requests targeting specified URLs. @Since { version = "0.30.0" } - headers: Mapping>>? + headers: Mapping>>? } /// Settings that control how Pkl talks to HTTP proxies. @@ -242,3 +242,55 @@ class ExternalReader { /// Additional command line arguments passed to the external reader process. arguments: Listing? } + +typealias ReservedHttpHeaderName = + "accept-charset" + | "accept-encoding" + | "access-control-request-headers" + | "access-control-request-method" + | "connection" + | "content-length" + | "cookie" + | "date" + | "dnt" + | "expect" + | "host" + | "keep-alive" + | "origin" + | "permissions-policy" + | "referer" + | "te" + | "trailer" + | "transfer-encoding" + | "upgrade" + | "via" + +const local ReservedHttpHeaderPrefix = new Listing { + "proxy-" + "sec-" + "access-control-" +} + +const local hasReservedHttpHeaderPrefix = (header: String) -> + ReservedHttpHeaderPrefix.any((it) -> header.startsWith(it)) + +const local httpHeaderNameRegex = Regex("^[a-zA-Z0-9!#\\$%&'*+-.^_`|~]+$") +const local hasValidHttpHeaderName = (header: String) -> + httpHeaderNameRegex.findMatchesIn(header) + +@Since {version = "0.30.0" } +typealias HttpHeaderName = String( + this == toLowerCase(), + !(this is ReservedHttpHeaderName), + !hasReservedHttpHeaderPrefix.apply(this), + hasValidHttpHeaderName +) + +const local httpHeaderValueRegex = Regex("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$") +const local hasValidHttpHeaderValue = (value : String) -> + httpHeaderValueRegex.findMatchesIn(value) + +@Since {version = "0.30.0"} +typealias HttpHeaderValue = String( + hasValidHttpHeaderValue +) From f805e2536300dbf375c4587cc76b0d51eb930077 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Tue, 3 Mar 2026 21:52:40 +0900 Subject: [PATCH 07/20] Update implementation based on the SPICE changes - Introduce pattern-based URL matches - Both accept a single and multiple values for header values - Move header append logic to RequestRewritingClient --- .../org/pkl/commons/cli/CliBaseOptions.kt | 2 +- .../kotlin/org/pkl/commons/cli/CliCommand.kt | 2 +- .../pkl/commons/cli/commands/BaseOptions.kt | 20 ++-- .../PklEvaluatorSettings.java | 59 +++++----- .../java/org/pkl/core/http/HttpClient.java | 5 +- .../org/pkl/core/http/HttpClientBuilder.java | 13 +-- .../java/org/pkl/core/http/JdkHttpClient.java | 20 +--- .../pkl/core/http/RequestRewritingClient.java | 23 +++- .../core/http/RequestRewritingClientTest.kt | 86 ++++++++++++++- .../org/pkl/core/settings/PklSettingsTest.kt | 9 +- .../java/org/pkl/gradle/task/BasePklTask.java | 2 +- stdlib/EvaluatorSettings.pkl | 101 +++++++++--------- 12 files changed, 219 insertions(+), 123 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt index 26f9cae6f..f19da557a 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt @@ -146,7 +146,7 @@ data class CliBaseOptions( val httpRewrites: Map? = null, /** HTTP headers to add to the request. */ - val httpHeaders: Map>>? = null, + val httpHeaders: List>>>? = null, /** External module reader process specs */ val externalModuleReaders: Map = mapOf(), diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt index 9b44fb622..8e74fded5 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt @@ -218,7 +218,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { cliOptions.httpRewrites ?: evaluatorSettings?.http?.rewrites ?: settings.http?.rewrites() } - private val httpHeaders: Map>>? by lazy { + private val httpHeaders: List>>>? by lazy { cliOptions.httpHeaders ?: project?.evaluatorSettings?.http?.headers ?: settings.http?.headers } diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index 9b24b2305..0dc0fa8d5 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -33,10 +33,10 @@ import org.pkl.commons.cli.CliException import org.pkl.commons.shlex import org.pkl.core.Pair as PPair import org.pkl.core.evaluatorSettings.Color -import org.pkl.core.evaluatorSettings.PklEvaluatorSettings import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.TraceMode import org.pkl.core.runtime.VmUtils +import org.pkl.core.util.GlobResolver import org.pkl.core.util.IoUtils @Suppress("MemberVisibilityCanBePrivate") @@ -95,6 +95,9 @@ class BaseOptions : OptionGroup() { Pair(it.first, ExternalReader(cmd.first(), cmd.drop(1))) } } + + val HEADER_NAME_REGEX = Pattern.compile("^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$") + val HEADER_VALUE_REGEX = Pattern.compile("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$") } private val defaults = CliBaseOptions() @@ -287,14 +290,14 @@ class BaseOptions : OptionGroup() { .multiple() .toMap() - val httpHeaders: Map>> by + val httpHeaders: List>>> by option( names = arrayOf("--http-headers"), - metavar = "=

:
[,
:
...]", + metavar = "=
:
[,
:
...]", help = "HTTP header to add to the request.", ) .convert { it -> - val (uriStr, headers) = + val (stringPattern, headers) = it.split("=", limit = 2).let { parts -> require(parts.size == 2) { "Headers must be in the form of =
:
" @@ -303,7 +306,7 @@ class BaseOptions : OptionGroup() { } try { - val uri = URI(uriStr.trim()) + var pattern = GlobResolver.toRegexPattern(stringPattern) val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") val headerPairs = @@ -311,15 +314,15 @@ class BaseOptions : OptionGroup() { val (headerName, headerValue) = headerRegex.find(header)?.destructured ?: fail("Header '$header' is not in 'name:value' format.") - require(PklEvaluatorSettings.HEADER_NAME_REGEX.matcher(headerName).matches()) { + require(HEADER_NAME_REGEX.matcher(headerName).matches()) { "HTTP header name '$headerName' has invalid syntax." } - require(PklEvaluatorSettings.HEADER_VALUE_REGEX.matcher(headerValue).matches()) { + require(HEADER_VALUE_REGEX.matcher(headerValue).matches()) { "HTTP header value '$headerValue' has invalid syntax" } PPair(headerName, headerValue) } - uri to headerPairs + PPair(pattern, headerPairs) } catch (e: IllegalArgumentException) { fail(e.message!!) } catch (e: URISyntaxException) { @@ -337,7 +340,6 @@ class BaseOptions : OptionGroup() { } } .multiple() - .toMap() val externalModuleReaders: Map by option( diff --git a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java index 9ba201450..13f3fe314 100644 --- a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -18,6 +18,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -27,6 +28,7 @@ import java.util.function.BiFunction; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.pkl.core.Duration; import org.pkl.core.PNull; import org.pkl.core.PObject; @@ -35,6 +37,8 @@ import org.pkl.core.PklException; import org.pkl.core.Value; import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.GlobResolver; +import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; import org.pkl.core.util.Nullable; /** Java version of {@code pkl.EvaluatorSettings}. */ @@ -54,10 +58,6 @@ public record PklEvaluatorSettings( @Nullable Map externalResourceReaders, @Nullable TraceMode traceMode) { - public static final Pattern HEADER_NAME_REGEX = Pattern.compile("^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$"); - public static final Pattern HEADER_VALUE_REGEX = - Pattern.compile("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$"); - /** Initializes a {@link PklEvaluatorSettings} from a raw object representation. */ @SuppressWarnings("unchecked") public static PklEvaluatorSettings parse( @@ -134,7 +134,7 @@ public static PklEvaluatorSettings parse( public record Http( @Nullable Proxy proxy, @Nullable Map rewrites, - @Nullable Map>> headers) { + @Nullable List>>> headers) { public static final Http DEFAULT = new Http(null, Collections.emptyMap(), null); @SuppressWarnings("unchecked") @@ -157,31 +157,36 @@ public record Http( } } } - var headers = http.getProperty("headers"); - HashMap>> parsedHeaders = null; - if (!(headers instanceof PNull)) { - parsedHeaders = new HashMap<>(); - var headersMap = (Map>>) headers; - for (var entry : headersMap.entrySet()) { - var uri = entry.getKey(); - var pairs = entry.getValue(); - for (var pair : pairs) { - if (!HEADER_NAME_REGEX.matcher(pair.getFirst()).matches()) { - throw new PklException(ErrorMessages.create("invalidHeaderName", pair.getFirst())); - } - if (!HEADER_VALUE_REGEX.matcher(pair.getSecond()).matches()) { - throw new PklException( - ErrorMessages.create("invalidHeaderValue", pair.getSecond())); - } - } + var headerDefs = http.getProperty("headers"); + List>>> parsedHeaderDefs = null; + if (!(headerDefs instanceof PNull)) { + parsedHeaderDefs = new ArrayList<>(); + var headerDefsMap = (Map>) headerDefs; + for (var entry : headerDefsMap.entrySet()) { + var stringPattern = entry.getKey(); + var headersMap = entry.getValue(); try { - parsedHeaders.put(new URI(uri), pairs); - } catch (URISyntaxException e) { - throw new PklException(ErrorMessages.create("invalidUri", e.getInput())); + var urlPattern = GlobResolver.toRegexPattern(stringPattern); + var pairs = + headersMap.entrySet().stream() + .flatMap( + header -> { + var value = header.getValue(); + if (value instanceof List) { + return ((List) value) + .stream().map(v -> new Pair(header.getKey(), v)); + } else { + return Stream.of(new Pair(header.getKey(), value)); + } + }) + .toList(); + parsedHeaderDefs.add(new Pair(urlPattern, pairs)); + } catch (InvalidGlobPatternException e) { + throw new PklException(ErrorMessages.create("invalidUri", stringPattern)); } } } - return new Http(proxy, parsedRewrites, parsedHeaders); + return new Http(proxy, parsedRewrites, parsedHeaderDefs); } else { throw PklBugException.unreachableCode(); } diff --git a/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java index 41d1faa91..352a37896 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/HttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -23,6 +23,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import org.pkl.core.Pair; import org.pkl.core.util.Nullable; @@ -157,7 +158,7 @@ interface Builder { *

This method clears all existing headers and replaces them with the contents of the * provided map. */ - Builder setHeaders(Map>> headers); + Builder setHeaders(List>>> headers); /** * Creates a new {@code HttpClient} from the current state of this builder. diff --git a/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java b/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java index 3935f02a7..99e2f8da2 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.function.Supplier; +import java.util.regex.Pattern; import org.pkl.core.Pair; import org.pkl.core.Release; import org.pkl.core.http.HttpClient.Builder; @@ -40,7 +41,7 @@ final class HttpClientBuilder implements HttpClient.Builder { private int testPort = -1; private ProxySelector proxySelector; private Map rewrites = new HashMap<>(); - private Map>> headers = new HashMap<>(); + private List>>> headers = new ArrayList<>(); HttpClientBuilder() { var release = Release.current(); @@ -113,7 +114,7 @@ public Builder addRewrite(URI sourcePrefix, URI targetPrefix) { } @Override - public Builder setHeaders(Map>> headers) { + public Builder setHeaders(List>>> headers) { this.headers = headers; return this; } @@ -135,9 +136,9 @@ private Supplier doBuild() { this.proxySelector != null ? this.proxySelector : java.net.ProxySelector.getDefault(); return () -> { var jdkClient = - new JdkHttpClient( - certificateFiles, certificateBytes, connectTimeout, proxySelector, headers); - return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient, rewrites); + new JdkHttpClient(certificateFiles, certificateBytes, connectTimeout, proxySelector); + return new RequestRewritingClient( + userAgent, requestTimeout, testPort, jdkClient, rewrites, headers); }; } } diff --git a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java index e4c0ee871..c22560e40 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -22,7 +22,6 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.ConnectException; -import java.net.URI; import java.net.http.HttpClient.Redirect; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -42,13 +41,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; import javax.annotation.concurrent.ThreadSafe; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManagerFactory; -import org.pkl.core.Pair; import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.Exceptions; @@ -57,7 +54,6 @@ final class JdkHttpClient implements HttpClient { // non-private for testing final java.net.http.HttpClient underlying; - final Map>> headers; // call java.net.http.HttpClient.close() if available (JDK 21+) private static final MethodHandle closeMethod; @@ -81,8 +77,7 @@ final class JdkHttpClient implements HttpClient { List certificateFiles, List certificateBytes, Duration connectTimeout, - java.net.ProxySelector proxySelector, - Map>> headers) { + java.net.ProxySelector proxySelector) { underlying = java.net.http.HttpClient.newBuilder() .sslContext(createSslContext(certificateFiles, certificateBytes)) @@ -90,22 +85,13 @@ final class JdkHttpClient implements HttpClient { .proxy(proxySelector) .followRedirects(Redirect.NORMAL) .build(); - this.headers = headers; } @Override public HttpResponse send(HttpRequest request, BodyHandler responseBodyHandler) throws IOException { try { - var wrappedRequestBuilder = HttpRequest.newBuilder(request, (name, value) -> true); - for (var entry : headers.entrySet()) { - if (RequestRewritingClient.matchesRewriteRule(request.uri(), entry.getKey())) { - for (var value : entry.getValue()) { - wrappedRequestBuilder.header(value.getFirst(), value.getSecond()); - } - } - } - return underlying.send(wrappedRequestBuilder.build(), responseBodyHandler); + return underlying.send(request, responseBodyHandler); } catch (ConnectException e) { // original exception has no message throw new ConnectException( diff --git a/pkl-core/src/main/java/org/pkl/core/http/RequestRewritingClient.java b/pkl-core/src/main/java/org/pkl/core/http/RequestRewritingClient.java index 7ff366a8c..8cf0e6bbf 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/RequestRewritingClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/RequestRewritingClient.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -28,7 +28,10 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; +import java.util.stream.Stream; import javax.annotation.concurrent.ThreadSafe; +import org.pkl.core.Pair; import org.pkl.core.PklBugException; import org.pkl.core.util.HttpUtils; import org.pkl.core.util.Nullable; @@ -54,6 +57,7 @@ final class RequestRewritingClient implements HttpClient { final int testPort; final HttpClient delegate; private final List> rewrites; + private final List>>> headers; private final AtomicBoolean closed = new AtomicBoolean(); @@ -62,7 +66,8 @@ final class RequestRewritingClient implements HttpClient { Duration requestTimeout, int testPort, HttpClient delegate, - Map rewrites) { + Map rewrites, + List>>> headers) { this.userAgent = userAgent; this.requestTimeout = requestTimeout; this.testPort = testPort; @@ -72,6 +77,7 @@ final class RequestRewritingClient implements HttpClient { .map((it) -> Map.entry(normalizeRewrite(it.getKey()), normalizeRewrite(it.getValue()))) .sorted(Comparator.comparingInt((it) -> -it.getKey().toString().length())) .toList(); + this.headers = headers; } @Override @@ -112,6 +118,9 @@ private HttpRequest rewriteRequest(HttpRequest original) { .map() .forEach((name, values) -> values.forEach(value -> builder.header(name, value))); builder.setHeader("User-Agent", userAgent); + for (var header : this.getHeaders(original.uri())) { + builder.header(header.getFirst(), header.getSecond()); + } var method = original.method(); original @@ -216,6 +225,16 @@ private URI rewriteUri(URI uri) { return ret; } + private List> getHeaders(URI uri) { + return headers.stream() + .flatMap( + rule -> + rule.getFirst().asPredicate().test(uri.toString()) + ? rule.getSecond().stream() + : Stream.empty()) + .toList(); + } + private void checkNotClosed(HttpRequest request) { if (closed.get()) { throw new IllegalStateException( diff --git a/pkl-core/src/test/kotlin/org/pkl/core/http/RequestRewritingClientTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/http/RequestRewritingClientTest.kt index 77ad41b25..a1176a83c 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/http/RequestRewritingClientTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/http/RequestRewritingClientTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -21,9 +21,11 @@ import java.net.http.HttpRequest import java.net.http.HttpRequest.BodyPublishers import java.net.http.HttpResponse.BodyHandlers import java.time.Duration +import java.util.regex.Pattern import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatList import org.junit.jupiter.api.Test +import org.pkl.core.Pair as PPair class RequestRewritingClientTest { private val captured = RequestCapturingClient() @@ -34,6 +36,7 @@ class RequestRewritingClientTest { -1, captured, mapOf(URI("https://foo/") to URI("https://bar/")), + listOf(), ) private val exampleUri = URI("https://example.com/foo/bar.html") private val exampleRequest = HttpRequest.newBuilder(exampleUri).build() @@ -121,7 +124,8 @@ class RequestRewritingClientTest { @Test fun `rewrites port 0 if test port is set`() { val captured = RequestCapturingClient() - val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured, mapOf()) + val client = + RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured, mapOf(), listOf()) val request = HttpRequest.newBuilder(URI("https://example.com:0")).build() client.send(request, BodyHandlers.discarding()) @@ -303,9 +307,85 @@ class RequestRewritingClientTest { private fun rewrittenRequest(uri: String, rules: Map): String { val captured = RequestCapturingClient() - val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured, rules) + val client = + RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured, rules, listOf()) val request = HttpRequest.newBuilder(URI(uri)).build() client.send(request, BodyHandlers.discarding()) return captured.request.uri().toString() } + + @Test + fun `adds configured headers for matching URI patterns`() { + val captured = RequestCapturingClient() + val client = + RequestRewritingClient( + "Pkl", + Duration.ofSeconds(42), + -1, + captured, + mapOf(), + listOf( + PPair(Pattern.compile("^https://example\\.com/.*"), listOf(PPair("x-one", "one"))), + PPair( + Pattern.compile("^https://example\\.com/foo/.*"), + listOf(PPair("x-two", "two-a"), PPair("x-two", "two-b")), + ), + ), + ) + val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build() + + client.send(request, BodyHandlers.discarding()) + + assertThatList(captured.request.headers().allValues("x-one")).containsExactly("one") + assertThatList(captured.request.headers().allValues("x-two")).containsExactly("two-a", "two-b") + } + + @Test + fun `does not add configured headers for non-matching URI patterns`() { + val captured = RequestCapturingClient() + val client = + RequestRewritingClient( + "Pkl", + Duration.ofSeconds(42), + -1, + captured, + mapOf(), + listOf( + PPair(Pattern.compile("^https://foo\\.com/.*"), listOf(PPair("x-foo", "foo"))), + PPair(Pattern.compile("^https://bar\\.com/.*"), listOf(PPair("x-bar", "bar"))), + ), + ) + val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build() + + client.send(request, BodyHandlers.discarding()) + + assertThat(captured.request.headers().firstValue("x-foo")).isEmpty + assertThat(captured.request.headers().firstValue("x-bar")).isEmpty + } + + @Test + fun `appends configured header values to existing request headers`() { + val captured = RequestCapturingClient() + val client = + RequestRewritingClient( + "Pkl", + Duration.ofSeconds(42), + -1, + captured, + mapOf(), + listOf( + PPair( + Pattern.compile("^https://example\\.com/.*"), + listOf(PPair("x-foo", "rule-a"), PPair("x-foo", "rule-b")), + ) + ), + ) + val request = + HttpRequest.newBuilder(URI("https://example.com/foo/bar")).header("x-foo", "request").build() + + client.send(request, BodyHandlers.discarding()) + + assertThatList(captured.request.headers().allValues("x-foo")) + .containsExactly("request", "rule-a", "rule-b") + } } diff --git a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt index 28072b986..654a334bb 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -29,6 +29,7 @@ import org.pkl.core.PObject import org.pkl.core.Pair as PPair import org.pkl.core.evaluatorSettings.PklEvaluatorSettings import org.pkl.core.settings.PklSettings.Editor +import org.pkl.core.util.GlobResolver class PklSettingsTest { @Test @@ -67,7 +68,7 @@ class PklSettingsTest { } headers { ["https://foo.com/"] { - Pair("X-Foo", "bar") + ["x-foo"] = "bar" } } } @@ -83,7 +84,9 @@ class PklSettingsTest { listOf("example.com", "pkg.pkl-lang.org"), ), mapOf(URI("https://foo.com/") to URI("https://bar.com/")), - mapOf(URI("https://foo.com/") to listOf(PPair("X-Foo", "bar"))), + listOf( + PPair(GlobResolver.toRegexPattern("https://foo.com/"), listOf(PPair("x-foo", "bar"))) + ), ) assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp)) diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java index 3e337d9d1..f628ec2a8 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java @@ -164,7 +164,7 @@ public Provider getEvalRootDirPath() { @Input @Optional - public abstract MapProperty>> getHttpHeaders(); + public abstract ListProperty>>> getHttpHeaders(); @Input @Optional diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index cfaabaf36..5972a5b36 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -126,10 +126,10 @@ local const hasNonEmptyHostname = (it: String) -> /// A key or value in [Http.rewrites]. @Since { version = "0.29.0" } -typealias HttpPrefix = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname) +typealias HttpRewrite = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname) -@Deprecated { since = "0.30.0"; replaceWith = "HttpPrefix" } -typealias HttpRewrite = HttpPrefix +@Since { version = "0.31.0" } +typealias UrlPattern = String(endsWith(Regex("[/*]"))) /// Settings that control how Pkl talks to HTTP(S) servers. class Http { @@ -171,11 +171,11 @@ class Http { /// An rewrite target should also not contain a query string or fragment component /// (not schematically enforced). @Since { version = "0.29.0" } - rewrites: Mapping? - + rewrites: Mapping? + /// HTTP headers to add to outbound requests targeting specified URLs. - @Since { version = "0.30.0" } - headers: Mapping>>? + @Since { version = "0.31.0" } + headers: Mapping | HttpHeaderValue>>? } /// Settings that control how Pkl talks to HTTP proxies. @@ -243,54 +243,53 @@ class ExternalReader { arguments: Listing? } -typealias ReservedHttpHeaderName = +typealias ReservedHttpHeaderName = "accept-charset" - | "accept-encoding" - | "access-control-request-headers" - | "access-control-request-method" - | "connection" - | "content-length" - | "cookie" - | "date" - | "dnt" - | "expect" - | "host" - | "keep-alive" - | "origin" - | "permissions-policy" - | "referer" - | "te" - | "trailer" - | "transfer-encoding" - | "upgrade" - | "via" - -const local ReservedHttpHeaderPrefix = new Listing { + | "accept-encoding" + | "access-control-request-headers" + | "access-control-request-method" + | "connection" + | "content-length" + | "cookie" + | "date" + | "dnt" + | "expect" + | "host" + | "keep-alive" + | "origin" + | "permissions-policy" + | "referer" + | "te" + | "trailer" + | "transfer-encoding" + | "upgrade" + | "via" + +local const ReservedHttpHeaderPrefix = new Listing { "proxy-" "sec-" "access-control-" } -const local hasReservedHttpHeaderPrefix = (header: String) -> +local const hasReservedHttpHeaderPrefix = (header: String) -> ReservedHttpHeaderPrefix.any((it) -> header.startsWith(it)) - -const local httpHeaderNameRegex = Regex("^[a-zA-Z0-9!#\\$%&'*+-.^_`|~]+$") -const local hasValidHttpHeaderName = (header: String) -> - httpHeaderNameRegex.findMatchesIn(header) - -@Since {version = "0.30.0" } -typealias HttpHeaderName = String( - this == toLowerCase(), - !(this is ReservedHttpHeaderName), - !hasReservedHttpHeaderPrefix.apply(this), - hasValidHttpHeaderName -) - -const local httpHeaderValueRegex = Regex("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$") -const local hasValidHttpHeaderValue = (value : String) -> - httpHeaderValueRegex.findMatchesIn(value) - -@Since {version = "0.30.0"} -typealias HttpHeaderValue = String( - hasValidHttpHeaderValue -) + +local const httpHeaderNameRegex = Regex("^[a-zA-Z0-9!#\\$%&'*+-.^_`|~]+$") +local const hasValidHttpHeaderName = (header: String) -> + !httpHeaderNameRegex.findMatchesIn(header).isEmpty + +@Since { version = "0.31.0" } +typealias HttpHeaderName = + String( + (it: String) -> it == toLowerCase(), + (it: String) -> !(it is ReservedHttpHeaderName), + (it: String) -> !hasReservedHttpHeaderPrefix.apply(it), + hasValidHttpHeaderName, + ) + +local const httpHeaderValueRegex = Regex("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$") +local const hasValidHttpHeaderValue = (value: String) -> + !httpHeaderValueRegex.findMatchesIn(value).isEmpty + +@Since { version = "0.31.0" } +typealias HttpHeaderValue = String(hasValidHttpHeaderValue) From 0e4078c97abc5fb867dd77341367df9b94379f3e Mon Sep 17 00:00:00 2001 From: kyokuping Date: Mon, 16 Mar 2026 21:56:10 +0900 Subject: [PATCH 08/20] Revert Copyright date change --- pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java index c22560e40..0758830c2 100644 --- a/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java +++ b/pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024 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. From 857cee044352676ca574354f53bc9198ac3cae6f Mon Sep 17 00:00:00 2001 From: kyokuping Date: Mon, 16 Mar 2026 21:59:32 +0900 Subject: [PATCH 09/20] Update `@Since` annotations --- stdlib/EvaluatorSettings.pkl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 5972a5b36..197528917 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -128,7 +128,7 @@ local const hasNonEmptyHostname = (it: String) -> @Since { version = "0.29.0" } typealias HttpRewrite = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname) -@Since { version = "0.31.0" } +@Since { version = "0.32.0" } typealias UrlPattern = String(endsWith(Regex("[/*]"))) /// Settings that control how Pkl talks to HTTP(S) servers. @@ -174,7 +174,7 @@ class Http { rewrites: Mapping? /// HTTP headers to add to outbound requests targeting specified URLs. - @Since { version = "0.31.0" } + @Since { version = "0.32.0" } headers: Mapping | HttpHeaderValue>>? } @@ -243,6 +243,7 @@ class ExternalReader { arguments: Listing? } +@Since { version = "0.32.0" } typealias ReservedHttpHeaderName = "accept-charset" | "accept-encoding" @@ -278,7 +279,7 @@ local const httpHeaderNameRegex = Regex("^[a-zA-Z0-9!#\\$%&'*+-.^_`|~]+$") local const hasValidHttpHeaderName = (header: String) -> !httpHeaderNameRegex.findMatchesIn(header).isEmpty -@Since { version = "0.31.0" } +@Since { version = "0.32.0" } typealias HttpHeaderName = String( (it: String) -> it == toLowerCase(), @@ -291,5 +292,5 @@ local const httpHeaderValueRegex = Regex("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$ local const hasValidHttpHeaderValue = (value: String) -> !httpHeaderValueRegex.findMatchesIn(value).isEmpty -@Since { version = "0.31.0" } +@Since { version = "0.32.0" } typealias HttpHeaderValue = String(hasValidHttpHeaderValue) From 72f1897d209d433e5ee488b6b891c2dcd045412a Mon Sep 17 00:00:00 2001 From: kyokuping Date: Mon, 16 Mar 2026 22:04:33 +0900 Subject: [PATCH 10/20] Rewrite lambdas as expressions --- stdlib/EvaluatorSettings.pkl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 197528917..ded34a4c9 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -282,15 +282,13 @@ local const hasValidHttpHeaderName = (header: String) -> @Since { version = "0.32.0" } typealias HttpHeaderName = String( - (it: String) -> it == toLowerCase(), - (it: String) -> !(it is ReservedHttpHeaderName), - (it: String) -> !hasReservedHttpHeaderPrefix.apply(it), + this == toLowerCase(), + !(this is ReservedHttpHeaderName), + !hasReservedHttpHeaderPrefix.apply(this), hasValidHttpHeaderName, ) local const httpHeaderValueRegex = Regex("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$") -local const hasValidHttpHeaderValue = (value: String) -> - !httpHeaderValueRegex.findMatchesIn(value).isEmpty @Since { version = "0.32.0" } -typealias HttpHeaderValue = String(hasValidHttpHeaderValue) +typealias HttpHeaderValue = String(!httpHeaderValueRegex.findMatchesIn(this).isEmpty) From 487d04ce2c3b6454cc62a3771525f94f8c89d7f9 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Mon, 16 Mar 2026 22:55:50 +0900 Subject: [PATCH 11/20] Disallow using commas as header separator --- .../pkl/commons/cli/commands/BaseOptions.kt | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index 0dc0fa8d5..4adfa5326 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -293,53 +293,39 @@ class BaseOptions : OptionGroup() { val httpHeaders: List>>> by option( names = arrayOf("--http-headers"), - metavar = "=

:
[,
:
...]", + metavar = "=
:
", help = "HTTP header to add to the request.", ) - .convert { it -> - val (stringPattern, headers) = - it.split("=", limit = 2).let { parts -> - require(parts.size == 2) { - "Headers must be in the form of =
:
" - } - parts[0] to parts[1] + .splitPair() + .transformAll { it -> + val headersMap = mutableMapOf>>() + + for ((stringPattern, header) in it) { + val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") + val (headerName, headerValue) = + headerRegex.find(header)?.destructured + ?: fail("Header '$header' is not in 'name:value' format.") + require(HEADER_NAME_REGEX.matcher(headerName).matches()) { + "HTTP header name '$headerName' has invalid syntax." } + require(HEADER_VALUE_REGEX.matcher(headerValue).matches()) { + "HTTP header value '$headerValue' has invalid syntax" + } + val headerPair = PPair(headerName, headerValue) + val headerPairList = headersMap[stringPattern] + if (headerPairList == null) { + headersMap[stringPattern] = mutableListOf(headerPair) + } else { + headerPairList.add(headerPair) + } + } try { - var pattern = GlobResolver.toRegexPattern(stringPattern) - - val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") - val headerPairs = - headers.split(',').map { header -> - val (headerName, headerValue) = - headerRegex.find(header)?.destructured - ?: fail("Header '$header' is not in 'name:value' format.") - require(HEADER_NAME_REGEX.matcher(headerName).matches()) { - "HTTP header name '$headerName' has invalid syntax." - } - require(HEADER_VALUE_REGEX.matcher(headerValue).matches()) { - "HTTP header value '$headerValue' has invalid syntax" - } - PPair(headerName, headerValue) - } - PPair(pattern, headerPairs) - } catch (e: IllegalArgumentException) { + headersMap.entries.map { PPair(GlobResolver.toRegexPattern(it.key), it.value) } + } catch (e: GlobResolver.InvalidGlobPatternException) { fail(e.message!!) - } catch (e: URISyntaxException) { - val message = buildString { - append("HTTP headers target `${e.input}` has invalid syntax (${e.reason}).") - if (e.index > -1) { - append("\n\n") - append(e.input) - append("\n") - append(" ".repeat(e.index)) - append("^") - } - } - fail(message) } } - .multiple() val externalModuleReaders: Map by option( From 39194ba923fcd4216f27297a3ce81c91ed69361a Mon Sep 17 00:00:00 2001 From: kyokuping Date: Tue, 17 Mar 2026 00:09:23 +0900 Subject: [PATCH 12/20] Implement strict header validation in CLI --- .../pkl/commons/cli/commands/BaseOptions.kt | 36 +++++----- .../main/java/org/pkl/core/util/IoUtils.java | 69 ++++++++++++++++++- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index 4adfa5326..f4ee5b534 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -300,28 +300,26 @@ class BaseOptions : OptionGroup() { .transformAll { it -> val headersMap = mutableMapOf>>() - for ((stringPattern, header) in it) { - val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") - val (headerName, headerValue) = - headerRegex.find(header)?.destructured - ?: fail("Header '$header' is not in 'name:value' format.") - require(HEADER_NAME_REGEX.matcher(headerName).matches()) { - "HTTP header name '$headerName' has invalid syntax." - } - require(HEADER_VALUE_REGEX.matcher(headerValue).matches()) { - "HTTP header value '$headerValue' has invalid syntax" - } - val headerPair = PPair(headerName, headerValue) - val headerPairList = headersMap[stringPattern] - if (headerPairList == null) { - headersMap[stringPattern] = mutableListOf(headerPair) - } else { - headerPairList.add(headerPair) + try { + for ((stringPattern, header) in it) { + val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") + val (headerName, headerValue) = + headerRegex.find(header)?.destructured + ?: fail("Header '$header' is not in 'name:value' format.") + IoUtils.validateHeaderName(headerName) + IoUtils.validateHeaderValue(headerValue) + val headerPair = PPair(headerName, headerValue) + val headerPairList = headersMap[stringPattern] + if (headerPairList == null) { + headersMap[stringPattern] = mutableListOf(headerPair) + } else { + headerPairList.add(headerPair) + } } - } - try { headersMap.entries.map { PPair(GlobResolver.toRegexPattern(it.key), it.value) } + } catch (e: IllegalArgumentException) { + fail(e.message!!) } catch (e: GlobResolver.InvalidGlobPatternException) { fail(e.message!!) } diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index 362fa5dda..18fe04aa6 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -52,6 +52,36 @@ public final class IoUtils { private static final Pattern windowsPathLike = Pattern.compile("\\w:\\\\.*"); + private static final Pattern headerNameLike = Pattern.compile("^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$"); + + private static final Pattern headerValueLike = + Pattern.compile("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$"); + + private static final String[] reservedHeaderNames = { + "accept-charset", + "accept-encoding", + "access-control-request-headers", + "access-control-request-method", + "connection", + "content-length", + "cookie", + "date", + "dnt", + "expect", + "host", + "keep-alive", + "origin", + "permissions-policy", + "referer", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "via" + }; + + private static final String[] reservedHeaderPrefixs = {"proxy-", "sec-", "access-control-"}; + private IoUtils() {} public static URL toUrl(URI uri) throws IOException { @@ -854,4 +884,41 @@ public static void validateRewriteRule(URI rewrite) { "Rewrite rule must end with '/', but was '%s'".formatted(rewrite)); } } + + private static boolean isReservedHeaderName(String headerName) { + return Arrays.stream(reservedHeaderNames).anyMatch((reserved) -> headerName.equals(reserved)); + } + + private static boolean hasReservedHeaderPrefix(String headerName) { + return Arrays.stream(reservedHeaderPrefixs).anyMatch((prefix) -> headerName.startsWith(prefix)); + } + + public static void validateHeaderName(String headerName) { + if (!headerName.equals(headerName.toLowerCase())) { + throw new IllegalArgumentException( + "HTTP header '%s' should be all lowercase".formatted(headerName)); + } + + if (isReservedHeaderName(headerName)) { + throw new IllegalArgumentException( + "HTTP header '%s' is a reserved header".formatted(headerName)); + } + + if (hasReservedHeaderPrefix(headerName)) { + throw new IllegalArgumentException( + "HTTP header '%s' starts with a reserved header prefix".formatted(headerName)); + } + + if (!headerNameLike.matcher(headerName).matches()) { + throw new IllegalArgumentException( + "HTTP header name '%s' has an invalid syntax".formatted(headerName)); + } + } + + public static void validateHeaderValue(String headerValue) { + if (headerValueLike.matcher(headerValue).matches()) { + throw new IllegalArgumentException( + "HTTP header value '%s' has an invalid syntax".formatted(headerValue)); + } + } } From f0c21a097cfb049e7699c63a22285f84517277c6 Mon Sep 17 00:00:00 2001 From: Jeaeun Kim <109906379+kyokuping@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:27:44 +0900 Subject: [PATCH 13/20] Use computeIfAbsent Co-authored-by: Jen Basch --- .../kotlin/org/pkl/commons/cli/commands/BaseOptions.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index f4ee5b534..274955c9a 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -308,13 +308,9 @@ class BaseOptions : OptionGroup() { ?: fail("Header '$header' is not in 'name:value' format.") IoUtils.validateHeaderName(headerName) IoUtils.validateHeaderValue(headerValue) - val headerPair = PPair(headerName, headerValue) - val headerPairList = headersMap[stringPattern] - if (headerPairList == null) { - headersMap[stringPattern] = mutableListOf(headerPair) - } else { - headerPairList.add(headerPair) - } + headersMap + .computeIfAbsent(stringPattern) { mutableListOf() } + .add(PPair(headerName, headerValue)) } headersMap.entries.map { PPair(GlobResolver.toRegexPattern(it.key), it.value) } From 97ee16b1af7be0656a7a4b06c8cad6705adeb56d Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 8 May 2026 17:13:48 +0900 Subject: [PATCH 14/20] move headerRegex compilation out of loop --- .../src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index 274955c9a..34980f890 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -301,8 +301,8 @@ class BaseOptions : OptionGroup() { val headersMap = mutableMapOf>>() try { + val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") for ((stringPattern, header) in it) { - val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") val (headerName, headerValue) = headerRegex.find(header)?.destructured ?: fail("Header '$header' is not in 'name:value' format.") From 6832d82967714f6cd508db975c031fa6b5958844 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 8 May 2026 17:37:07 +0900 Subject: [PATCH 15/20] remove unused regex --- .../main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index 34980f890..a4cae35f9 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -95,9 +95,6 @@ class BaseOptions : OptionGroup() { Pair(it.first, ExternalReader(cmd.first(), cmd.drop(1))) } } - - val HEADER_NAME_REGEX = Pattern.compile("^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$") - val HEADER_VALUE_REGEX = Pattern.compile("^[\\t\\u0020-\\u007E\\u0080-\\u00FF]*$") } private val defaults = CliBaseOptions() From 729673b86b6641c7322ba866a8813a5815b624d3 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 8 May 2026 17:39:46 +0900 Subject: [PATCH 16/20] fix typos --- pkl-core/src/main/java/org/pkl/core/util/IoUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index 18fe04aa6..3aaa63ab2 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -80,7 +80,7 @@ public final class IoUtils { "via" }; - private static final String[] reservedHeaderPrefixs = {"proxy-", "sec-", "access-control-"}; + private static final String[] reservedHeaderPrefixes = {"proxy-", "sec-", "access-control-"}; private IoUtils() {} @@ -576,7 +576,7 @@ public static ServiceLoader createServiceLoader(Class serviceClass) { } // don't use ServiceLoader.load(Class) - // because loading services from thread context class loader doesn't work inside gradle plugins + // because loading services from thread context class loader doesn't work inside Gradle plugins return ServiceLoader.load(serviceClass, IoUtils.class.getClassLoader()); } @@ -890,7 +890,7 @@ private static boolean isReservedHeaderName(String headerName) { } private static boolean hasReservedHeaderPrefix(String headerName) { - return Arrays.stream(reservedHeaderPrefixs).anyMatch((prefix) -> headerName.startsWith(prefix)); + return Arrays.stream(reservedHeaderPrefixes).anyMatch((prefix) -> headerName.startsWith(prefix)); } public static void validateHeaderName(String headerName) { From c14818c5fd7d2160fcba7cfbee5ec4b92a1ee341 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 8 May 2026 17:43:57 +0900 Subject: [PATCH 17/20] update reservedHeaderNames list --- pkl-core/src/main/java/org/pkl/core/util/IoUtils.java | 2 -- stdlib/EvaluatorSettings.pkl | 2 -- 2 files changed, 4 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index 3aaa63ab2..09764a60d 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -60,8 +60,6 @@ public final class IoUtils { private static final String[] reservedHeaderNames = { "accept-charset", "accept-encoding", - "access-control-request-headers", - "access-control-request-method", "connection", "content-length", "cookie", diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index ded34a4c9..ab9472d4c 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -247,8 +247,6 @@ class ExternalReader { typealias ReservedHttpHeaderName = "accept-charset" | "accept-encoding" - | "access-control-request-headers" - | "access-control-request-method" | "connection" | "content-length" | "cookie" From 2433bc3dc023fcbe3d2b2c6ef6393b8ce3fc7fca Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 8 May 2026 17:58:35 +0900 Subject: [PATCH 18/20] remove lowercase enforcement for header names in CLI --- pkl-core/src/main/java/org/pkl/core/util/IoUtils.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index 09764a60d..fc31abab3 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -892,10 +892,6 @@ private static boolean hasReservedHeaderPrefix(String headerName) { } public static void validateHeaderName(String headerName) { - if (!headerName.equals(headerName.toLowerCase())) { - throw new IllegalArgumentException( - "HTTP header '%s' should be all lowercase".formatted(headerName)); - } if (isReservedHeaderName(headerName)) { throw new IllegalArgumentException( From e4c47540cd87197bfe6b637d5d78d2d97fcfd7cd Mon Sep 17 00:00:00 2001 From: kyokuping Date: Fri, 8 May 2026 18:00:29 +0900 Subject: [PATCH 19/20] apply spotless formatting --- .../kotlin/org/pkl/cli/CliEvaluatorTest.kt | 310 +++++++++--------- .../org/pkl/commons/cli/CliBaseOptions.kt | 7 +- .../main/java/org/pkl/core/util/IoUtils.java | 3 +- .../org/pkl/core/settings/PklSettingsTest.kt | 18 +- .../main/kotlin/org/pkl/doc/PageGenerator.kt | 10 +- .../org/pkl/server/AbstractServerTest.kt | 152 ++++----- 6 files changed, 251 insertions(+), 249 deletions(-) diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt index 4001d77e3..3dc89a7b0 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt @@ -58,7 +58,7 @@ class CliEvaluatorTest { name = "pigeon" age = 20 + 10 } - """ + """ .trimIndent() private val packageServer = PackageServer() @@ -243,12 +243,12 @@ person: .resolve("test2.pkl") .writeString( """ - amends "test.pkl" - - person { - name = "barn owl" - } - """ + amends "test.pkl" + + person { + name = "barn owl" + } + """ .trimIndent() ) @@ -655,9 +655,9 @@ result = someLib.x "output.pcf", """ x = 1 - + y = 2 - + z = 3 """ .trimIndent(), @@ -739,7 +739,7 @@ result = someLib.x } } } - """ + """ .trimIndent(), ) ) @@ -815,31 +815,31 @@ result = someLib.x writePklFile( "test0.pkl", """ - output { - files { - ["foo.pcf"] { - value = new Dynamic { - ["bar"] = "baz" + output { + files { + ["foo.pcf"] { + value = new Dynamic { + ["bar"] = "baz" + } } } } - } - """ + """ .trimIndent(), ), writePklFile( "test1.pkl", """ - output { - files { - ["bar.pcf"] { - value = new Dynamic { - ["bar"] = "baz" + output { + files { + ["bar.pcf"] { + value = new Dynamic { + ["bar"] = "baz" + } } } } - } - """ + """ .trimIndent(), ), ) @@ -860,27 +860,27 @@ result = someLib.x writePklFile( "bar.pkl", """ - output { - files { - ["foo.pcf"] { - text = "myBar" + output { + files { + ["foo.pcf"] { + text = "myBar" + } } } - } - """ + """ .trimIndent(), ), writePklFile( "foo.pkl", """ - output { - files { - ["foo.pcf"] { - text = "myFoo" + output { + files { + ["foo.pcf"] { + text = "myFoo" + } } } - } - """ + """ .trimIndent(), ), ) @@ -918,7 +918,7 @@ result = someLib.x } } } - """ + """ .trimIndent(), ) val options = @@ -949,7 +949,7 @@ result = someLib.x } } } - """ + """ .trimIndent(), ) val options = @@ -970,23 +970,23 @@ result = someLib.x writePklFile( "test1.pkl", """ - output { - files { - ["."] { text = "bar" } + output { + files { + ["."] { text = "bar" } + } } - } - """ + """ .trimIndent(), ), writePklFile( "test2.pkl", """ - output { - files { - ["myDir"] { text = "bar" } + output { + files { + ["myDir"] { text = "bar" } + } } - } - """ + """ .trimIndent(), ), ) @@ -1009,23 +1009,23 @@ result = someLib.x writePklFile( "test1.pkl", """ - output { - files { - ["foo.txt"] { text = "bar" } + output { + files { + ["foo.txt"] { text = "bar" } + } } - } - """ + """ .trimIndent(), ), writePklFile( "test2.pkl", """ - output { - files { - ["foo.txt"] { text = "bar" } + output { + files { + ["foo.txt"] { text = "bar" } + } } - } - """ + """ .trimIndent(), ), ) @@ -1045,13 +1045,13 @@ result = someLib.x writePklFile( "test.pkl", """ - output { - files { - ["foo.txt"] { text = "bar" } - ["./foo.txt"] { text = "bar" } + output { + files { + ["foo.txt"] { text = "bar" } + ["./foo.txt"] { text = "bar" } + } } - } - """ + """ .trimIndent(), ) val options = @@ -1071,12 +1071,12 @@ result = someLib.x writePklFile( "test.pkl", """ - output { - files { - ["foo:bar"] { text = "bar" } + output { + files { + ["foo:bar"] { text = "bar" } + } } - } - """ + """ .trimIndent(), ) @@ -1096,12 +1096,12 @@ result = someLib.x writePklFile( "test.pkl", """ - output { - files { - ["foo\\bar"] { text = "bar" } + output { + files { + ["foo\\bar"] { text = "bar" } + } } - } - """ + """ .trimIndent(), ) @@ -1120,10 +1120,10 @@ result = someLib.x writePklFile( "test.pkl", """ - foo { - bar = 1 - } - """ + foo { + bar = 1 + } + """ .trimIndent(), ) val options = @@ -1136,8 +1136,8 @@ result = someLib.x assertThat(buffer.toString(StandardCharsets.UTF_8)) .isEqualTo( """ - new Dynamic { bar = 1 } - """ + new Dynamic { bar = 1 } + """ .trimIndent() ) } @@ -1148,13 +1148,13 @@ result = someLib.x writePklFile( "test.pkl", """ - class Person { - name: String + class Person { + name: String - function toString() = "Person(\(name))" - } - person: Person = new { name = "Frodo" } - """ + function toString() = "Person(\(name))" + } + person: Person = new { name = "Frodo" } + """ .trimIndent(), ) val options = @@ -1173,10 +1173,10 @@ result = someLib.x writePklFile( "test.pkl", """ - person { - friend { name = "Bilbo" } - } - """ + person { + friend { name = "Bilbo" } + } + """ .trimIndent(), ) val options = @@ -1196,17 +1196,17 @@ result = someLib.x writePklFile( "test.pkl", """ - res = 1 - """ + res = 1 + """ .trimIndent(), ) writePklFile( "PklProject", """ amends "pkl:Project" - + package = throw("invalid project package") - """ + """ .trimIndent(), ) val options = @@ -1225,8 +1225,8 @@ result = someLib.x writePklFile( "test.pkl", """ - res = read*("env:**") - """ + res = read*("env:**") + """ .trimIndent(), ) writePklFile( @@ -1234,14 +1234,14 @@ result = someLib.x // language=Pkl """ amends "pkl:Project" - + evaluatorSettings { env { ["foo"] = "foo" ["bar"] = "bar" } } - """ + """ .trimIndent(), ) val options = @@ -1251,12 +1251,12 @@ result = someLib.x assertThat(buffer.toString(StandardCharsets.UTF_8)) .isEqualTo( """ - res { - ["env:bar"] = "bar" - ["env:foo"] = "foo" - } - - """ + res { + ["env:bar"] = "bar" + ["env:foo"] = "foo" + } + + """ .trimIndent() ) } @@ -1353,10 +1353,10 @@ result = someLib.x writePklFile( "test.pkl", """ - import "package://localhost:0/birds@0.5.0#/catalog/Swallow.pkl" - - res = Swallow - """ + import "package://localhost:0/birds@0.5.0#/catalog/Swallow.pkl" + + res = Swallow + """ .trimIndent(), ) val buffer = ByteArrayOutputStream() @@ -1375,14 +1375,14 @@ result = someLib.x assertThat(buffer.toString(StandardCharsets.UTF_8)) .isEqualTo( """ - res { - name = "Swallow" - favoriteFruit { - name = "Apple" + res { + name = "Swallow" + favoriteFruit { + name = "Apple" + } } - } - - """ + + """ .trimIndent() ) assertThat(tempDir.resolve("package-2")).doesNotExist() @@ -1481,13 +1481,13 @@ result = someLib.x assertThat(output) .isEqualTo( """ - name = "Ostrich" + name = "Ostrich" - favoriteFruit { - name = "Orange" - } + favoriteFruit { + name = "Orange" + } - """ + """ .trimIndent() ) verify(getRequestedFor(urlEqualTo("birds@0.5.0"))) @@ -1590,13 +1590,13 @@ result = someLib.x homeDir.writeFile( "settings.pkl", """ - amends "pkl:settings" + amends "pkl:settings" - http { - proxy { - address = "http://invalid.proxy.address" - } + http { + proxy { + address = "http://invalid.proxy.address" } + } """ .trimIndent(), ) @@ -1677,26 +1677,26 @@ result = someLib.x writePklFile( "test.pkl", """ - pigeon { - name = "Pigeon" - diet = "Seeds" - } - parrot { - name = "Parrot" - diet = "Seeds" - } - output { - files { - ["pigeon.json"] { - value = pigeon - renderer = new JsonRenderer {} - } - ["birds/parrot.yaml"] { - value = parrot - renderer = new YamlRenderer {} - } + pigeon { + name = "Pigeon" + diet = "Seeds" + } + parrot { + name = "Parrot" + diet = "Seeds" + } + output { + files { + ["pigeon.json"] { + value = pigeon + renderer = new JsonRenderer {} + } + ["birds/parrot.yaml"] { + value = parrot + renderer = new YamlRenderer {} } } + } """ .trimIndent(), ) @@ -1713,10 +1713,10 @@ result = someLib.x realOutputDir.resolve("pigeon.json"), "pigeon.json", """ - { - "name": "Pigeon", - "diet": "Seeds" - } + { + "name": "Pigeon", + "diet": "Seeds" + } """ .trimIndent(), ) @@ -1725,8 +1725,8 @@ result = someLib.x realOutputDir.resolve("birds/parrot.yaml"), "parrot.yaml", """ - name: Parrot - diet: Seeds + name: Parrot + diet: Seeds """ .trimIndent(), ) @@ -1735,10 +1735,10 @@ result = someLib.x symlinkOutputDir.resolve("pigeon.json"), "pigeon.json", """ - { - "name": "Pigeon", - "diet": "Seeds" - } + { + "name": "Pigeon", + "diet": "Seeds" + } """ .trimIndent(), ) @@ -1747,8 +1747,8 @@ result = someLib.x symlinkOutputDir.resolve("birds/parrot.yaml"), "parrot.yaml", """ - name: Parrot - diet: Seeds + name: Parrot + diet: Seeds """ .trimIndent(), ) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt index f19da557a..9a9a303ce 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt @@ -196,10 +196,9 @@ data class CliBaseOptions( // sort modules to make cli output independent of source module order .sorted() - val normalizedSettingsModule: URI? = - settings?.let { uri -> - if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri) - } + val normalizedSettingsModule: URI? = settings?.let { uri -> + if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri) + } /** [modulePath] after normalization. */ val normalizedModulePath: List? = modulePath?.map(normalizedWorkingDir::resolve) diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index fc31abab3..d35e25a63 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -888,7 +888,8 @@ private static boolean isReservedHeaderName(String headerName) { } private static boolean hasReservedHeaderPrefix(String headerName) { - return Arrays.stream(reservedHeaderPrefixes).anyMatch((prefix) -> headerName.startsWith(prefix)); + return Arrays.stream(reservedHeaderPrefixes) + .anyMatch((prefix) -> headerName.startsWith(prefix)); } public static void validateHeaderName(String headerName) { diff --git a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt index 654a334bb..fd52e3ea5 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/settings/PklSettingsTest.kt @@ -140,15 +140,15 @@ class PklSettingsTest { evaluator.evaluate( ModuleSource.text( """ - import "pkl:settings" - - system = settings.System - idea = settings.Idea - textMate = settings.TextMate - sublime = settings.Sublime - atom = settings.Atom - vsCode = settings.VsCode - """ + import "pkl:settings" + + system = settings.System + idea = settings.Idea + textMate = settings.TextMate + sublime = settings.Sublime + atom = settings.Atom + vsCode = settings.VsCode + """ .trimIndent() ) ) diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt index 1a2c37efa..fee8d4c1e 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt @@ -723,11 +723,13 @@ internal abstract class PageGenerator( ?.let { markdownRenderer.render(markdownParser.parse(it)).trim().ifEmpty { null } } } - private val deprecatedAnnotation: PObject? = - annotations.find { it.classInfo == PClassInfo.Deprecated } + private val deprecatedAnnotation: PObject? = annotations.find { + it.classInfo == PClassInfo.Deprecated + } - private val alsoKnownAsAnnotation: PObject? = - annotations.find { it.classInfo == PClassInfo.AlsoKnownAs } + private val alsoKnownAsAnnotation: PObject? = annotations.find { + it.classInfo == PClassInfo.AlsoKnownAs + } val isDeprecatedMember: Boolean = deprecatedAnnotation != null diff --git a/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt b/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt index b4037f8c5..da3c1e770 100644 --- a/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt +++ b/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt @@ -105,7 +105,7 @@ abstract class AbstractServerTest { foo { bar = "bar" } - """ + """ .trimIndent(), null, ) @@ -132,7 +132,7 @@ abstract class AbstractServerTest { URI("repl:text"), """ foo = trace(1 + 2 + 3) - """ + """ .trimIndent(), null, ) @@ -159,7 +159,7 @@ abstract class AbstractServerTest { function foo() = 5 result = foo() - """ + """ .trimIndent(), null, ) @@ -286,7 +286,7 @@ abstract class AbstractServerTest { URI("repl:text"), """ res = read*("bird:/**.txt").keys - """ + """ .trimIndent(), "res", ) @@ -315,11 +315,11 @@ abstract class AbstractServerTest { assertThat(evaluateResponse.result?.debugRendering) .isEqualTo( """ - - 6 - - - - 'bird:/foo.txt' - - 'bird:/subdir/bar.txt' - """ + - 6 + - + - 'bird:/foo.txt' + - 'bird:/subdir/bar.txt' + """ .trimIndent() ) } @@ -335,7 +335,7 @@ abstract class AbstractServerTest { URI("repl:text"), """ res = read*("bird:/**.txt").keys - """ + """ .trimIndent(), "res", ) @@ -371,7 +371,7 @@ abstract class AbstractServerTest { URI("repl:text"), """ res = read*("bird:/**.txt").keys - """ + """ .trimIndent(), "res", ) @@ -390,19 +390,19 @@ abstract class AbstractServerTest { assertThat(evaluateResponse.error) .isEqualTo( """ - –– Pkl Error –– - I/O error resolving glob pattern `bird:/**.txt`. - IOException: didnt work - - 1 | res = read*("bird:/**.txt").keys - ^^^^^^^^^^^^^^^^^^^^^ - at text#res (repl:text) - - 1 | res - ^^^ - at (repl:text) - - """ + –– Pkl Error –– + I/O error resolving glob pattern `bird:/**.txt`. + IOException: didnt work + + 1 | res = read*("bird:/**.txt").keys + ^^^^^^^^^^^^^^^^^^^^^ + at text#res (repl:text) + + 1 | res + ^^^ + at (repl:text) + + """ .trimIndent() ) } @@ -549,14 +549,14 @@ abstract class AbstractServerTest { assertThat(evaluateResponse.result?.debugRendering) .isEqualTo( """ - - 6 - - - - 'bird:/Person.pkl' - - 'bird:/birds/parrot.pkl' - - 'bird:/birds/pigeon.pkl' - - 'bird:/majesticBirds/barnOwl.pkl' - - 'bird:/majesticBirds/elfOwl.pkl' - """ + - 6 + - + - 'bird:/Person.pkl' + - 'bird:/birds/parrot.pkl' + - 'bird:/birds/pigeon.pkl' + - 'bird:/majesticBirds/barnOwl.pkl' + - 'bird:/majesticBirds/elfOwl.pkl' + """ .trimIndent() ) } @@ -582,9 +582,9 @@ abstract class AbstractServerTest { assertThat(evaluateResponse.result?.debugRendering) .isEqualTo( """ - - 6 - - [] - """ + - 6 + - [] + """ .trimIndent() ) } @@ -612,19 +612,19 @@ abstract class AbstractServerTest { assertThat(evaluateResponse.error) .isEqualTo( """ - –– Pkl Error –– - I/O error resolving glob pattern `bird:/**.pkl`. - IOException: nope - - 1 | res = import*("bird:/**.pkl").keys - ^^^^^^^^^^^^^^^^^^^^^^^ - at text#res (repl:text) - - 1 | res - ^^^ - at (repl:text) - - """ + –– Pkl Error –– + I/O error resolving glob pattern `bird:/**.pkl`. + IOException: nope + + 1 | res = import*("bird:/**.pkl").keys + ^^^^^^^^^^^^^^^^^^^^^^^ + at text#res (repl:text) + + 1 | res + ^^^ + at (repl:text) + + """ .trimIndent() ) } @@ -688,9 +688,9 @@ abstract class AbstractServerTest { URI("bird:/foo/bar/baz.pkl"), """ import ".../buz.pkl" - + res = buz.res - """ + """ .trimIndent(), "res", ) @@ -747,9 +747,9 @@ abstract class AbstractServerTest { readModuleRequest.requestId, evaluatorId, """ - firstName = "Pigeon" - lastName = "Bird" - fullName = firstName + " " + lastName + firstName = "Pigeon" + lastName = "Bird" + fullName = firstName + " " + lastName """ .trimIndent(), null, @@ -766,7 +766,7 @@ abstract class AbstractServerTest { lastName = "Bird" fullName = "Pigeon Bird" - """ + """ .trimIndent() ) } @@ -788,9 +788,9 @@ abstract class AbstractServerTest { response11.requestId, evaluatorId, """ - firstName = "Pigeon" - lastName = "Bird" - fullName = firstName + " " + lastName + firstName = "Pigeon" + lastName = "Bird" + fullName = firstName + " " + lastName """ .trimIndent(), null, @@ -807,7 +807,7 @@ abstract class AbstractServerTest { lastName = "Bird" fullName = "Pigeon Bird" - """ + """ .trimIndent() ) @@ -819,9 +819,9 @@ abstract class AbstractServerTest { response21.requestId, evaluatorId, """ - firstName = "Parrot" - lastName = "Bird" - fullName = firstName + " " + lastName + firstName = "Parrot" + lastName = "Bird" + fullName = firstName + " " + lastName """ .trimIndent(), null, @@ -838,7 +838,7 @@ abstract class AbstractServerTest { lastName = "Bird" fullName = "Parrot Bird" - """ + """ .trimIndent() ) } @@ -948,23 +948,23 @@ abstract class AbstractServerTest { .resolve("lib.pkl") .writeText( """ - text = "This is from lib" - """ + text = "This is from lib" + """ .trimIndent() ) libDir .resolve("PklProject") .writeText( """ - amends "pkl:Project" - - package { - name = "lib" - baseUri = "package://localhost:0/lib" - version = "5.0.0" - packageZipUrl = "https://localhost:0/lib.zip" - } - """ + amends "pkl:Project" + + package { + name = "lib" + baseUri = "package://localhost:0/lib" + version = "5.0.0" + packageZipUrl = "https://localhost:0/lib.zip" + } + """ .trimIndent() ) val projectDir = tempDir.resolve("proj/").createDirectories() @@ -973,14 +973,14 @@ abstract class AbstractServerTest { """ import "@birds/Bird.pkl" import "@lib/lib.pkl" - + res: Bird = new { name = "Birdie" favoriteFruit { name = "dragonfruit" } } - + libContents = lib - """ + """ .trimIndent() ) val dollar = '$' From 5620d562536f1eadbe57bb7a2011a1f3c19ad80a Mon Sep 17 00:00:00 2001 From: Jeaeun Kim <109906379+kyokuping@users.noreply.github.com> Date: Mon, 11 May 2026 17:37:25 +0900 Subject: [PATCH 20/20] fix inverted regex condition in IoUtils Co-authored-by: Islon Scherer --- pkl-core/src/main/java/org/pkl/core/util/IoUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java index d35e25a63..61bcc5f61 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/IoUtils.java @@ -911,7 +911,7 @@ public static void validateHeaderName(String headerName) { } public static void validateHeaderValue(String headerValue) { - if (headerValueLike.matcher(headerValue).matches()) { + if (!headerValueLike.matcher(headerValue).matches()) { throw new IllegalArgumentException( "HTTP header value '%s' has an invalid syntax".formatted(headerValue)); }