Keep a route's headers on the HTTPS upgrade its probe follows

A route probe follows an upgrade to HTTPS on the server's host, as the API client does, but the health client dropped the route's own headers -- an access token for the proxy in front, say -- from any request away from the route's origin, that upgrade included. The server answered the probe's second request with its sign-in, and a working route was taken not to answer. The headers now go where the API client sends them: the route's origin, and that upgrade on its host. The logout fence still keeps the cookie off.
This commit is contained in:
cogwheel 2026-10-08 15:05:31 +05:30
parent 4edc41d9e6
commit 9e99008ada
2 changed files with 60 additions and 2 deletions

View file

@ -696,8 +696,15 @@ Dio createConnectivityHealthClient(
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (ConnectivityService._originKey(options.uri) !=
ConnectivityService._originKey(serverUri)) {
// Kept where the API client keeps them: the server's own origin,
// or an upgrade to HTTPS on its host that a probe follows.
final target = options.uri;
final sameServer =
ConnectivityService._originKey(target) ==
ConnectivityService._originKey(serverUri) ||
(serverUri != null &&
isCredentialSafeRedirectTarget(serverUri, target));
if (!sameServer) {
options.headers.removeWhere(
(name, _) => customHeaderNames.contains(name.toLowerCase()),
);

View file

@ -223,6 +223,28 @@ void main() {
}, _RealHttpOverrides());
});
// A probe follows an upgrade to HTTPS on the server's host, as the API
// client does; the route's own headers go with it, and nowhere else.
test('health client keeps the route headers on an upgrade to HTTPS', () async {
final dio = createConnectivityHealthClient(
const ServerConfig(
id: 'route',
name: 'Route',
url: 'http://chat.example',
customHeaders: {'CF-Access-Client-Id': 'route-id'},
),
);
addTearDown(dio.close);
final sent = <String, Object?>{};
dio.httpClientAdapter = _HeaderRecorder(sent);
await dio.getUri<dynamic>(Uri.parse('https://chat.example/health'));
await dio.getUri<dynamic>(Uri.parse('https://other.example/health'));
check(sent['https://chat.example/health']).equals('route-id');
check(sent['https://other.example/health']).isNull();
});
group('a route probe', () {
/// Serves [handle] on a loopback port for the length of the test.
Future<String> serve(
@ -734,3 +756,32 @@ final class _FailingHealthAdapter implements _RequestCountingAdapter {
}
final class _RealHttpOverrides extends HttpOverrides {}
/// Records the route header each request went out with, by URL.
final class _HeaderRecorder implements HttpClientAdapter {
_HeaderRecorder(this.sent);
final Map<String, Object?> sent;
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
sent[options.uri.toString()] = options.headers.entries
.where((entry) => entry.key.toLowerCase() == 'cf-access-client-id')
.map((entry) => entry.value)
.firstOrNull;
return ResponseBody.fromString(
'{"status":true}',
HttpStatus.ok,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
}
@override
void close({bool force = false}) {}
}