Add support for HTTP rewrites (#1062)

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

Follows the design of https://github.com/apple/pkl-evolution/pull/17
This commit is contained in:
Daniel Chao
2025-07-16 15:53:31 -07:00
committed by GitHub
parent fea031a138
commit 99020bb79d
27 changed files with 607 additions and 47 deletions

View File

@@ -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.
@@ -140,6 +140,9 @@ data class CliBaseOptions(
/** Hostnames, IP addresses, or CIDR blocks to not proxy. */
val httpNoProxy: List<String>? = null,
/** URL prefixes to rewrite. */
val httpRewrites: Map<URI, URI>? = null,
/** External module reader process specs */
val externalModuleReaders: Map<String, ExternalReader> = mapOf(),

View File

@@ -15,6 +15,7 @@
*/
package org.pkl.commons.cli
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Pattern
@@ -166,29 +167,36 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
protected val useColor: Boolean by lazy { cliOptions.color?.hasColor() ?: false }
private val proxyAddress by lazy {
private val proxyAddress: URI? by lazy {
cliOptions.httpProxy
?: project?.evaluatorSettings?.http?.proxy?.address
?: settings.http?.proxy?.address
}
private val noProxy by lazy {
private val noProxy: List<String>? by lazy {
cliOptions.httpNoProxy
?: project?.evaluatorSettings?.http?.proxy?.noProxy
?: settings.http?.proxy?.noProxy
}
private val externalModuleReaders by lazy {
private val httpRewrites: Map<URI, URI>? by lazy {
cliOptions.httpRewrites
?: project?.evaluatorSettings?.http?.rewrites
?: settings.http?.rewrites()
}
private val externalModuleReaders: Map<String, PklEvaluatorSettings.ExternalReader> by lazy {
(project?.evaluatorSettings?.externalModuleReaders ?: emptyMap()) +
cliOptions.externalModuleReaders
}
private val externalResourceReaders by lazy {
private val externalResourceReaders: Map<String, PklEvaluatorSettings.ExternalReader> by lazy {
(project?.evaluatorSettings?.externalResourceReaders ?: emptyMap()) +
cliOptions.externalResourceReaders
}
private val externalProcesses by lazy {
private val externalProcesses:
Map<PklEvaluatorSettings.ExternalReader, ExternalReaderProcess> by lazy {
// Share ExternalReaderProcess instances between configured external resource/module readers
// with the same spec. This avoids spawning multiple subprocesses if the same reader implements
// both reader types and/or multiple schemes.
@@ -232,6 +240,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
if ((proxyAddress ?: noProxy) != null) {
setProxy(proxyAddress, noProxy ?: listOf())
}
httpRewrites?.let(::setRewrites)
// 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.

View File

@@ -235,6 +235,39 @@ class BaseOptions : OptionGroup() {
.single()
.split(",")
val httpRewrites: Map<URI, URI> by
option(
names = arrayOf("--http-rewrite"),
metavar = "from=to",
help = "URL prefixes that should be rewritten.",
)
.convert { it ->
val uris = it.split("=", limit = 2)
require(uris.size == 2) { "Rewrites must be in the form of <from>=<to>" }
try {
val (fromSpec, toSpec) = uris
val fromUri = URI(fromSpec).also { IoUtils.validateRewriteRule(it) }
val toUri = URI(toSpec).also { IoUtils.validateRewriteRule(it) }
fromUri to toUri
} catch (e: IllegalArgumentException) {
fail(e.message!!)
} catch (e: URISyntaxException) {
val message = buildString {
append("Rewrite 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<String, ExternalReader> by
option(
names = arrayOf("--external-module-reader"),
@@ -289,6 +322,7 @@ class BaseOptions : OptionGroup() {
caCertificates = caCertificates,
httpProxy = proxy,
httpNoProxy = noProxy ?: emptyList(),
httpRewrites = httpRewrites.ifEmpty { null },
externalModuleReaders = externalModuleReaders,
externalResourceReaders = externalResourceReaders,
)