fix: make bulwark respect server limits
If you have a large number of tags, getTagCounts would not be able to get the unread counts because it did not respect maxCallsInRequest, even though the value was actually read out, it was just ignored. There are also other places where the limits were not respected. Batching is now generalized in a helper that also takes maxObjectsInSet, which also was ignored, into account and is applied to all functions. In addition, the dev mock now also advertises and enforces the limits, so these issues can get picked up during development. Possible closes #699 Possibly closes #399
This commit is contained in:
@@ -2038,6 +2038,35 @@ function resolveBackReferences(
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request limits
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Stalwart's defaults. Too many method calls fails the request whole
|
||||
// (RFC 8620 §3.6.1), an over-sized /get or /set fails that call
|
||||
// (`requestTooLarge`, §5.1 and §5.3). The mock enforces what it advertises so a
|
||||
// client that sends an unsplit batch fails here the way it fails in production.
|
||||
const MAX_CALLS_IN_REQUEST = 16;
|
||||
const MAX_OBJECTS_IN_GET = 500;
|
||||
const MAX_OBJECTS_IN_SET = 500;
|
||||
|
||||
/** Objects a /set call touches, across all three of its maps (RFC 8620 §5.3). */
|
||||
function setObjectCount(args: MethodArgs): number {
|
||||
const size = (value: unknown) => (Array.isArray(value) ? value.length : Object.keys(value || {}).length);
|
||||
return size(args.create) + size(args.update) + size(args.destroy);
|
||||
}
|
||||
|
||||
/** The method-level error a server returns for an over-sized /get or /set. */
|
||||
function tooLargeFor(method: string, args: MethodArgs, callId: string): MethodResult | null {
|
||||
if (method.endsWith('/get') && Array.isArray(args.ids) && args.ids.length > MAX_OBJECTS_IN_GET) {
|
||||
return ['error', { type: 'requestTooLarge', description: `More than ${MAX_OBJECTS_IN_GET} ids in ${method}` }, callId];
|
||||
}
|
||||
if (method.endsWith('/set') && setObjectCount(args) > MAX_OBJECTS_IN_SET) {
|
||||
return ['error', { type: 'requestTooLarge', description: `More than ${MAX_OBJECTS_IN_SET} objects in ${method}` }, callId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2070,9 +2099,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
maxConcurrentUpload: 4,
|
||||
maxSizeRequest: 10000000,
|
||||
maxConcurrentRequests: 4,
|
||||
maxCallsInRequest: 16,
|
||||
maxObjectsInGet: 500,
|
||||
maxObjectsInSet: 500,
|
||||
maxCallsInRequest: MAX_CALLS_IN_REQUEST,
|
||||
maxObjectsInGet: MAX_OBJECTS_IN_GET,
|
||||
maxObjectsInSet: MAX_OBJECTS_IN_SET,
|
||||
collationAlgorithms: ['i;ascii-casemap', 'i;ascii-numeric', 'i;unicode-casemap'],
|
||||
},
|
||||
'urn:ietf:params:jmap:mail': {},
|
||||
@@ -2218,6 +2247,15 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'Invalid request: missing methodCalls' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (methodCalls.length > MAX_CALLS_IN_REQUEST) {
|
||||
return NextResponse.json({
|
||||
type: 'urn:ietf:params:jmap:error:limit',
|
||||
status: 400,
|
||||
limit: 'maxCallsInRequest',
|
||||
detail: `This request contains ${methodCalls.length} method calls, the maximum is ${MAX_CALLS_IN_REQUEST}.`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const responses: MethodResult[] = [];
|
||||
|
||||
// Process method calls sequentially (to support back-references)
|
||||
@@ -2227,8 +2265,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
// Use resolved args if available, otherwise original
|
||||
const args = i < resolved.length ? resolved[i][1] : methodCalls[i][1];
|
||||
|
||||
const tooLarge = tooLargeFor(method, args, callId);
|
||||
const handler = METHOD_HANDLERS[method];
|
||||
if (handler) {
|
||||
if (tooLarge) {
|
||||
responses.push(tooLarge);
|
||||
} else if (handler) {
|
||||
const result = handler(args, callId);
|
||||
responses.push(result);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user