Polish http rewrites (#1133)

* Polish rewrite docs
* Add documentation comments, add missing evaluator options
* Add ability to set HTTP builder in ConfigEvaluatorBuilder
* Add ability to set rewrites in executor API
This commit is contained in:
Daniel Chao
2025-07-22 15:42:07 -07:00
committed by GitHub
parent fe064960b4
commit 306a3b0fc2
7 changed files with 225 additions and 54 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import org.pkl.config.java.mapper.ValueMapperBuilder;
import org.pkl.core.EvaluatorBuilder; import org.pkl.core.EvaluatorBuilder;
import org.pkl.core.SecurityManager; import org.pkl.core.SecurityManager;
import org.pkl.core.StackFrameTransformer; import org.pkl.core.StackFrameTransformer;
import org.pkl.core.http.HttpClient;
import org.pkl.core.project.DeclaredDependencies; import org.pkl.core.project.DeclaredDependencies;
import org.pkl.core.project.Project; import org.pkl.core.project.Project;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@@ -242,6 +243,15 @@ public final class ConfigEvaluatorBuilder {
return this; return this;
} }
/**
* Returns the currently set evaluation timeout.
*
* <p>This is a convenience method that delegates to the underlying evaluator builder.
*/
public @Nullable Duration getTimeout() {
return evaluatorBuilder.getTimeout();
}
/** /**
* Sets the set of URI patterns to be allowed when importing modules. * Sets the set of URI patterns to be allowed when importing modules.
* *
@@ -305,12 +315,24 @@ public final class ConfigEvaluatorBuilder {
} }
/** /**
* Returns the currently set evaluation timeout. * Sets the HTTP Client to be used.
* *
* <p>This is a convenience method that delegates to the underlying evaluator builder. * <p>Defaults to {@code HttpClient.builder().buildLazily()}.
*
* @since 0.29.0
*/ */
public @Nullable Duration getTimeout() { public ConfigEvaluatorBuilder setHttpClient(HttpClient httpClient) {
return evaluatorBuilder.getTimeout(); evaluatorBuilder.setHttpClient(httpClient);
return this;
}
/**
* Returns the currently set HTTP client.
*
* @since 0.29.0
*/
public HttpClient getHttpClient() {
return evaluatorBuilder.getHttpClient();
} }
/** /**

View File

@@ -119,8 +119,35 @@ public interface HttpClient extends AutoCloseable {
*/ */
Builder setProxy(@Nullable URI proxyAddress, List<String> noProxy); Builder setProxy(@Nullable URI proxyAddress, List<String> noProxy);
/**
* Removes any existing rewrites, then adds the given rewrites.
*
* <p>A rewrite changes outbound HTTP URLs by replacing a source prefix with a targert prefix.
*
* <p>Each rewrite URI must start with {@code http://} or {@code https://}, and end with {@code
* /}.
*
* <p>Each key describes the prefix of a request, and each value describes the replacement
* prefix.
*
* <p>This can be useful for setting up mirroring of packages, which are fetched over HTTPS.
*
* <p>In the case of multiple matches, the longest prefix is used.
*
* <p>The URL hostname is case-insensitive.
*
* @throws IllegalArgumentException if {@code rewrites} is invalid.
* @since 0.29.0
*/
Builder setRewrites(Map<URI, URI> rewrites); Builder setRewrites(Map<URI, URI> rewrites);
/**
* Adds a rewrite rule.
*
* @see Builder#setRewrites(Map)
* @throws IllegalArgumentException if {@code sourcePrefix} or {@code targetPrefix} is invalid.
* @since 0.29.0
*/
Builder addRewrite(URI sourcePrefix, URI targetPrefix); Builder addRewrite(URI sourcePrefix, URI targetPrefix);
/** /**

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -17,14 +17,13 @@ package org.pkl.core.service;
import static org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME; import static org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME;
import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.*; import org.pkl.core.*;
@@ -37,6 +36,7 @@ import org.pkl.executor.spi.v1.ExecutorSpi;
import org.pkl.executor.spi.v1.ExecutorSpiException; import org.pkl.executor.spi.v1.ExecutorSpiException;
import org.pkl.executor.spi.v1.ExecutorSpiOptions; import org.pkl.executor.spi.v1.ExecutorSpiOptions;
import org.pkl.executor.spi.v1.ExecutorSpiOptions2; import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
public final class ExecutorSpiImpl implements ExecutorSpi { public final class ExecutorSpiImpl implements ExecutorSpi {
private static final int MAX_HTTP_CLIENTS = 3; private static final int MAX_HTTP_CLIENTS = 3;
@@ -142,8 +142,14 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
private HttpClient getOrCreateHttpClient(ExecutorSpiOptions options) { private HttpClient getOrCreateHttpClient(ExecutorSpiOptions options) {
List<Path> certificateFiles; List<Path> certificateFiles;
List<byte[]> certificateBytes; List<byte[]> certificateBytes;
Map<URI, URI> rewrites;
int testPort; int testPort;
try { try {
if (options instanceof ExecutorSpiOptions3 options3) {
rewrites = options3.getHttpRewrites();
} else {
rewrites = Map.of();
}
if (options instanceof ExecutorSpiOptions2 options2) { if (options instanceof ExecutorSpiOptions2 options2) {
certificateFiles = options2.getCertificateFiles(); certificateFiles = options2.getCertificateFiles();
certificateBytes = options2.getCertificateBytes(); certificateBytes = options2.getCertificateBytes();
@@ -153,14 +159,15 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
certificateBytes = List.of(); certificateBytes = List.of();
testPort = -1; testPort = -1;
} }
// host pkl-executor does not have class ExecutorOptions2 defined. // host pkl-executor does not have class ExecutorOptions2/ExecutorOptions3 defined.
// this will happen if the pkl-executor distribution is too old. // this will happen if the pkl-executor distribution is too old.
} catch (NoClassDefFoundError e) { } catch (NoClassDefFoundError e) {
certificateFiles = List.of(); certificateFiles = List.of();
certificateBytes = List.of(); certificateBytes = List.of();
rewrites = Map.of();
testPort = -1; testPort = -1;
} }
var clientKey = new HttpClientKey(certificateFiles, certificateBytes, testPort); var clientKey = new HttpClientKey(certificateFiles, certificateBytes, testPort, rewrites);
return httpClients.computeIfAbsent( return httpClients.computeIfAbsent(
clientKey, clientKey,
(key) -> { (key) -> {
@@ -171,6 +178,7 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
for (var bytes : key.certificateBytes) { for (var bytes : key.certificateBytes) {
builder.addCertificates(bytes); builder.addCertificates(bytes);
} }
builder.setRewrites(key.rewrites);
builder.setTestPort(key.testPort); builder.setTestPort(key.testPort);
// If the above didn't add any certificates, // If the above didn't add any certificates,
// builder will use the JVM's default SSL context. // builder will use the JVM's default SSL context.
@@ -178,35 +186,9 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
}); });
} }
private static final class HttpClientKey { private record HttpClientKey(
final Set<Path> certificateFiles; List<Path> certificateFiles,
final Set<byte[]> certificateBytes; List<byte[]> certificateBytes,
final int testPort; int testPort,
Map<URI, URI> rewrites) {}
HttpClientKey(List<Path> certificateFiles, List<byte[]> certificateBytes, int testPort) {
// also serves as defensive copy
this.certificateFiles = Set.copyOf(certificateFiles);
this.certificateBytes = Set.copyOf(certificateBytes);
this.testPort = testPort;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
HttpClientKey that = (HttpClientKey) obj;
return certificateFiles.equals(that.certificateFiles)
&& certificateBytes.equals(that.certificateBytes)
&& testPort == that.testPort;
}
@Override
public int hashCode() {
return Objects.hash(certificateFiles, certificateBytes, testPort);
}
}
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/ */
package org.pkl.executor; package org.pkl.executor;
import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
@@ -22,6 +23,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import org.pkl.executor.spi.v1.ExecutorSpiOptions; import org.pkl.executor.spi.v1.ExecutorSpiOptions;
import org.pkl.executor.spi.v1.ExecutorSpiOptions2; import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
/** /**
* Options for {@link Executor#evaluatePath}. * Options for {@link Executor#evaluatePath}.
@@ -53,6 +55,8 @@ public final class ExecutorOptions {
private final List<byte[]> certificateBytes; private final List<byte[]> certificateBytes;
private final Map<URI, URI> httpRewrites;
private final int testPort; // -1 means disabled private final int testPort; // -1 means disabled
private final int spiOptionsVersion; // -1 means use latest private final int spiOptionsVersion; // -1 means use latest
@@ -84,6 +88,7 @@ public final class ExecutorOptions {
private /* @Nullable */ Path projectDir; private /* @Nullable */ Path projectDir;
private List<Path> certificateFiles = List.of(); private List<Path> certificateFiles = List.of();
private List<byte[]> certificateBytes = List.of(); private List<byte[]> certificateBytes = List.of();
private Map<URI, URI> httpRewrites = Map.of();
private int testPort = -1; // -1 means disabled private int testPort = -1; // -1 means disabled
private int spiOptionsVersion = -1; // -1 means use latest private int spiOptionsVersion = -1; // -1 means use latest
@@ -197,6 +202,18 @@ public final class ExecutorOptions {
return this; return this;
} }
/**
* API equivalent of the {@code --http-rewrite} CLI option.
*
* <p>This option is ignored on Pkl 0.28 and older.
*
* @since 0.29.0
*/
public Builder httpRewrites(Map<URI, URI> httpRewrites) {
this.httpRewrites = httpRewrites;
return this;
}
/** Internal test option. -1 means disabled. */ /** Internal test option. -1 means disabled. */
Builder testPort(int testPort) { Builder testPort(int testPort) {
this.testPort = testPort; this.testPort = testPort;
@@ -223,6 +240,7 @@ public final class ExecutorOptions {
projectDir, projectDir,
certificateFiles, certificateFiles,
certificateBytes, certificateBytes,
httpRewrites,
testPort, testPort,
spiOptionsVersion); spiOptionsVersion);
} }
@@ -271,6 +289,7 @@ public final class ExecutorOptions {
projectDir, projectDir,
List.of(), List.of(),
List.of(), List.of(),
Map.of(),
-1, -1,
-1); -1);
} }
@@ -288,6 +307,7 @@ public final class ExecutorOptions {
/* @Nullable */ Path projectDir, /* @Nullable */ Path projectDir,
List<Path> certificateFiles, List<Path> certificateFiles,
List<byte[]> certificateBytes, List<byte[]> certificateBytes,
Map<URI, URI> httpRewrites,
int testPort, int testPort,
int spiOptionsVersion) { int spiOptionsVersion) {
@@ -303,6 +323,7 @@ public final class ExecutorOptions {
this.projectDir = projectDir; this.projectDir = projectDir;
this.certificateFiles = List.copyOf(certificateFiles); this.certificateFiles = List.copyOf(certificateFiles);
this.certificateBytes = List.copyOf(certificateBytes); this.certificateBytes = List.copyOf(certificateBytes);
this.httpRewrites = Map.copyOf(httpRewrites);
this.testPort = testPort; this.testPort = testPort;
this.spiOptionsVersion = spiOptionsVersion; this.spiOptionsVersion = spiOptionsVersion;
} }
@@ -374,6 +395,17 @@ public final class ExecutorOptions {
return certificateBytes; return certificateBytes;
} }
/**
* API equivalent of the {@code --http-rewrite} CLI option.
*
* <p>This option is ignored on Pkl 0.28 and older.
*
* @since 0.29.0
*/
public Map<URI, URI> getHttpRewrites() {
return httpRewrites;
}
@Override @Override
public boolean equals(/* @Nullable */ Object obj) { public boolean equals(/* @Nullable */ Object obj) {
if (this == obj) return true; if (this == obj) return true;
@@ -392,6 +424,7 @@ public final class ExecutorOptions {
&& Objects.equals(projectDir, other.projectDir) && Objects.equals(projectDir, other.projectDir)
&& Objects.equals(certificateFiles, other.certificateFiles) && Objects.equals(certificateFiles, other.certificateFiles)
&& Objects.equals(certificateBytes, other.certificateBytes) && Objects.equals(certificateBytes, other.certificateBytes)
&& Objects.equals(httpRewrites, other.httpRewrites)
&& testPort == other.testPort && testPort == other.testPort
&& spiOptionsVersion == other.spiOptionsVersion; && spiOptionsVersion == other.spiOptionsVersion;
} }
@@ -411,6 +444,7 @@ public final class ExecutorOptions {
projectDir, projectDir,
certificateFiles, certificateFiles,
certificateBytes, certificateBytes,
httpRewrites,
testPort, testPort,
spiOptionsVersion); spiOptionsVersion);
} }
@@ -442,6 +476,8 @@ public final class ExecutorOptions {
+ certificateFiles + certificateFiles
+ ", certificateBytes=" + ", certificateBytes="
+ certificateBytes + certificateBytes
+ ", httpRewrites="
+ httpRewrites
+ ", testPort=" + ", testPort="
+ testPort + testPort
+ ", spiOptionsVersion=" + ", spiOptionsVersion="
@@ -451,7 +487,23 @@ public final class ExecutorOptions {
ExecutorSpiOptions toSpiOptions() { ExecutorSpiOptions toSpiOptions() {
return switch (spiOptionsVersion) { return switch (spiOptionsVersion) {
case -1, 2 -> case -1, 3 ->
new ExecutorSpiOptions3(
allowedModules,
allowedResources,
environmentVariables,
externalProperties,
modulePath,
rootDir,
timeout,
outputFormat,
moduleCacheDir,
projectDir,
certificateFiles,
certificateBytes,
testPort,
httpRewrites);
case 2 ->
new ExecutorSpiOptions2( new ExecutorSpiOptions2(
allowedModules, allowedModules,
allowedResources, allowedResources,

View File

@@ -0,0 +1,63 @@
/*
* Copyright © 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.executor.spi.v1;
import java.net.URI;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
public class ExecutorSpiOptions3 extends ExecutorSpiOptions2 {
private final Map<URI, URI> httpRewrites;
public ExecutorSpiOptions3(
List<String> allowedModules,
List<String> allowedResources,
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
Path rootDir,
Duration timeout,
String outputFormat,
Path moduleCacheDir,
Path projectDir,
List<Path> certificateFiles,
List<byte[]> certificateBytes,
int testPort,
Map<URI, URI> httpRewrites) {
super(
allowedModules,
allowedResources,
environmentVariables,
externalProperties,
modulePath,
rootDir,
timeout,
outputFormat,
moduleCacheDir,
projectDir,
certificateFiles,
certificateBytes,
testPort);
this.httpRewrites = httpRewrites;
}
public Map<URI, URI> getHttpRewrites() {
return httpRewrites;
}
}

View File

@@ -16,12 +16,15 @@
package org.pkl.executor package org.pkl.executor
import java.io.File import java.io.File
import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.writeText
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
@@ -64,9 +67,9 @@ class EmbeddedExecutorTest {
// This context has a pkl-executor version that is lower than the distribution version. // This context has a pkl-executor version that is lower than the distribution version.
TestExecutor(executor1_2.value, 1, "SpiOptions1, Executor1, Distribution2"), TestExecutor(executor1_2.value, 1, "SpiOptions1, Executor1, Distribution2"),
TestExecutor(executor2_1.value, 1, "SpiOptions1, Executor2, Distribution1"), TestExecutor(executor2_1.value, 1, "SpiOptions1, Executor2, Distribution1"),
TestExecutor(executor2_1.value, 2, "SpiOptions2, Executor2, Distribution1"), TestExecutor(executor2_1.value, 3, "SpiOptions3, Executor2, Distribution1"),
TestExecutor(executor2_2.value, 1, "SpiOptions1, Executor2, Distribution2"), TestExecutor(executor2_2.value, 1, "SpiOptions1, Executor2, Distribution2"),
TestExecutor(executor2_2.value, 2, "SpiOptions2, Executor2, Distribution2"), TestExecutor(executor2_2.value, 3, "SpiOptions3, Executor2, Distribution2"),
) )
} }
@@ -81,19 +84,19 @@ class EmbeddedExecutorTest {
} }
// A pkl-executor library that supports ExecutorSpiOptions up to v1 // A pkl-executor library that supports ExecutorSpiOptions up to v1
// and a Pkl distribution that supports ExecutorSpiOptions up to v2. // and a Pkl distribution that supports ExecutorSpiOptions up to v3.
private val executor1_2: Lazy<Executor> = lazy { private val executor1_2: Lazy<Executor> = lazy {
EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader1) EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader1)
} }
// A pkl-executor library that supports ExecutorSpiOptions up to v2 // A pkl-executor library that supports ExecutorSpiOptions up to v3
// and a Pkl distribution that supports ExecutorSpiOptions up to v1. // and a Pkl distribution that supports ExecutorSpiOptions up to v1.
private val executor2_1: Lazy<Executor> = lazy { private val executor2_1: Lazy<Executor> = lazy {
EmbeddedExecutor(listOf(pklDistribution1), pklExecutorClassLoader2) EmbeddedExecutor(listOf(pklDistribution1), pklExecutorClassLoader2)
} }
// A pkl-executor library that supports ExecutorSpiOptions up to v2 // A pkl-executor library that supports ExecutorSpiOptions up to v3
// and a Pkl distribution that supports ExecutorSpiOptions up to v2. // and a Pkl distribution that supports ExecutorSpiOptions up to v3.
private val executor2_2: Lazy<Executor> = lazy { private val executor2_2: Lazy<Executor> = lazy {
EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader2) EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader2)
} }
@@ -103,11 +106,11 @@ class EmbeddedExecutorTest {
// a pkl-executor class loader that supports ExecutorSpiOptions up to v1 // a pkl-executor class loader that supports ExecutorSpiOptions up to v1
private val pklExecutorClassLoader1: ClassLoader by lazy { private val pklExecutorClassLoader1: ClassLoader by lazy {
FilteringClassLoader(pklExecutorClassLoader2) { className -> FilteringClassLoader(pklExecutorClassLoader2) { className ->
!className.endsWith("ExecutorSpiOptions2") !className.matches(Regex(".*ExecutorSpiOptions\\d+$"))
} }
} }
// a pkl-executor class loader that supports ExecutorSpiOptions up to v2 // a pkl-executor class loader that supports ExecutorSpiOptions up to v3
private val pklExecutorClassLoader2: ClassLoader by lazy { private val pklExecutorClassLoader2: ClassLoader by lazy {
EmbeddedExecutor::class.java.classLoader EmbeddedExecutor::class.java.classLoader
} }
@@ -127,7 +130,7 @@ class EmbeddedExecutorTest {
.apply { if (!exists()) missingTestFixture() } .apply { if (!exists()) missingTestFixture() }
} }
// a Pkl distribution that supports ExecutorSpiOptions up to v2 // a Pkl distribution that supports ExecutorSpiOptions up to v3
private val pklDistribution2: Path by lazy { private val pklDistribution2: Path by lazy {
FileTestUtils.rootProjectDir FileTestUtils.rootProjectDir
.resolve( .resolve(
@@ -556,6 +559,28 @@ class EmbeddedExecutorTest {
assertThat(cacheDir.toFile().list()).isNotEmpty() assertThat(cacheDir.toFile().list()).isNotEmpty()
} }
@Test
fun `http rewrites option`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.writeText(
"""
@ModuleInfo { minPklVersion = "0.29.0" }
result = import("https://example.com/foo.pkl")
"""
.trimIndent()
)
assertThatCode {
currentExecutor.evaluatePath(pklFile) {
allowedModules("file:", "https:")
allowedResources("prop:")
httpRewrites(mapOf(URI("https://example.com/") to URI("https://example.example/")))
}
}
.hasMessageContaining(
"Error connecting to host `example.example`. (request was rewritten: https://example.com/foo.pkl -> https://example.example/foo.pkl)"
)
}
@ParameterizedTest @ParameterizedTest
@MethodSource("getAllTestExecutors") @MethodSource("getAllTestExecutors")
@DisabledOnOs(OS.WINDOWS, disabledReason = "Can't populate legacy cache dir on Windows") @DisabledOnOs(OS.WINDOWS, disabledReason = "Can't populate legacy cache dir on Windows")

View File

@@ -133,8 +133,8 @@ class Http {
/// Each key describes the prefix of a request, and each value describes the replacement prefix. /// Each key describes the prefix of a request, and each value describes the replacement prefix.
/// ///
/// This can be useful for setting up mirroring of packages, which are fetched over HTTPS. /// This can be useful for setting up mirroring of packages, which are fetched over HTTPS.
// ///
/// In the case of multiple matches, the longest prefix is is used. /// In the case of multiple matches, the longest prefix is used.
/// ///
/// The URL hostname is case-insensitive. /// The URL hostname is case-insensitive.
/// ///