mirror of
https://github.com/block/buzz.git
synced 2026-11-04 16:31:38 +00:00
Mobile read GET /api/join-policy through the generic 64 KiB hosted response cap, but the relay allows each policy markdown document up to 256 KiB and Desktop accepts a 4 MiB policy response. A self-hosted relay with a valid policy over 64 KiB was joinable on Desktop but failed on Mobile with an invalid-response error before consent could render, and every retry hit the same cap. boundedHttpResponse and readJoinResponse now take a maxBytes that defaults to the existing 64 KiB hostedResponseMaxBytes. Only loadJoinPolicy opts into the 4 MiB cap that matches Desktop, so the accept-policy, claim, and other hosted replies keep their limit. The reader accumulates into a BytesBuilder so a multi-MiB body is not held as a list of ints. The invite consent journey now serves a policy over 64 KiB and asserts that Mobile loads its full terms and claims only after native acceptance. The hosted-admission "oversized policy" row used bare whitespace, which failed as invalid JSON whatever the cap was. It now sends valid JSON padded past 4 MiB. A focused test serves a relay-sized policy alongside an accept-policy reply over 64 KiB: the policy reaches consent, the reply is rejected, and no claim is sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
56 lines
1.6 KiB
Dart
56 lines
1.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
/// The body cap for hosted responses that do not declare their own.
|
|
const hostedResponseMaxBytes = 64 * 1024;
|
|
|
|
/// One deadline covers headers and the bounded body. Abort only this request,
|
|
/// leaving the shared client usable for sign-out and subsequent attempts.
|
|
Future<http.Response> boundedHttpResponse(
|
|
http.Client client,
|
|
http.Request source, {
|
|
int maxBytes = hostedResponseMaxBytes,
|
|
}) async {
|
|
final abort = Completer<void>();
|
|
final request =
|
|
http.AbortableRequest(
|
|
source.method,
|
|
source.url,
|
|
abortTrigger: abort.future,
|
|
)
|
|
..followRedirects = false
|
|
..headers.addAll(source.headers)
|
|
..bodyBytes = source.bodyBytes;
|
|
StreamIterator<List<int>>? body;
|
|
Future<http.Response> read() async {
|
|
final response = await client.send(request);
|
|
final iterator = body = StreamIterator(response.stream);
|
|
if (abort.isCompleted) {
|
|
await iterator.cancel();
|
|
throw http.RequestAbortedException();
|
|
}
|
|
final bytes = BytesBuilder();
|
|
while (await iterator.moveNext()) {
|
|
if (bytes.length + iterator.current.length > maxBytes) {
|
|
throw const FormatException('Response too large');
|
|
}
|
|
bytes.add(iterator.current);
|
|
}
|
|
return http.Response.bytes(
|
|
bytes.takeBytes(),
|
|
response.statusCode,
|
|
headers: response.headers,
|
|
);
|
|
}
|
|
|
|
try {
|
|
return await read().timeout(const Duration(seconds: 30));
|
|
} finally {
|
|
abort.complete();
|
|
// Do not let transport cleanup hold the account's serial queue hostage.
|
|
unawaited(body?.cancel());
|
|
}
|
|
}
|