Skip to content

Commit e703479

Browse files
dhr-vermaCopilot
andauthored
Adds support to restrict HTTP response sizes (microsoft#23440)
## Description This PR adds a new module - `ResponseSizeMiddleware`. This class has a method to validate response sizes to ensure that their byte sizes is below a given a `maxResponseSizeInMegaBytes`. If the response body is greater than `maxResponseSizeInMegaBytes`, an `HTTP 413` is sent as a response, else the original response is sent back to the client. This is introduced to primarily enforce response sizes in the storage stack (Historian and Gitrest). A future PR will add this module to both Gitrest and Historian after the r11 changes are merged. Historian and Gitrest both have request size limitations. Hence, this PR just adds support to limit response sizes. This is added to reduce the chances of Gitrest returning very large responses that could result in Historian running OOM. --------- Co-authored-by: Copilot <[email protected]>
1 parent 8470945 commit e703479

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

server/routerlicious/packages/services-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,4 @@ export {
6262
IRedisClientConnectionManager,
6363
} from "./redisClientConnectionManager";
6464
export { ITenantKeyGenerator, TenantKeyGenerator } from "./tenantKeyGenerator";
65+
export { ResponseSizeMiddleware } from "./responseSizeMiddleware";
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { Lumberjack } from "@fluidframework/server-services-telemetry";
7+
import type { RequestHandler } from "express";
8+
9+
export class ResponseSizeMiddleware {
10+
constructor(private readonly maxResponseSizeInMegaBytes: number) {}
11+
12+
public validateResponseSize(): RequestHandler {
13+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
14+
return async (req, res, next) => {
15+
const originalSend = res.send;
16+
res.send = (body) => {
17+
let responseSize: number;
18+
try {
19+
responseSize = Buffer.byteLength(
20+
typeof body === "string" ? body : JSON.stringify(body),
21+
);
22+
} catch (error) {
23+
Lumberjack.error("Invalid JSON string in response body", undefined, error);
24+
// In case of JSON parsing errors, we log internally and send the
25+
// original response to the client to prevent breaking the client's experience.
26+
return originalSend.call(res, body);
27+
}
28+
29+
if (responseSize > this.maxResponseSizeInMegaBytes * 1024 * 1024) {
30+
Lumberjack.error(
31+
`Response size of ${responseSize} bytes, exceeds the maximum allowed size of ${this.maxResponseSizeInMegaBytes} megabytes`,
32+
);
33+
return res.status(413).json({
34+
error: "Response too large",
35+
message: `Response size exceeds the maximum allowed size of ${this.maxResponseSizeInMegaBytes} megabytes`,
36+
});
37+
}
38+
return originalSend.call(res, body);
39+
};
40+
next();
41+
};
42+
}
43+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import assert from "assert";
7+
import express from "express";
8+
import request from "supertest";
9+
import { ResponseSizeMiddleware } from "../responseSizeMiddleware";
10+
11+
describe("Throttler Middleware", () => {
12+
const endpoint = "/test";
13+
const route = `${endpoint}/:id?`;
14+
let responseSizeMiddleware: ResponseSizeMiddleware;
15+
const responseMaxSizeInMb = 1; // 1MB
16+
let app: express.Application;
17+
let supertest: request.SuperTest<request.Test>;
18+
const setUpRoute = (data: any, subPath?: string): void => {
19+
const routePath = `${route}${subPath ? `/${subPath}` : ""}`;
20+
app.get(routePath, (req, res) => {
21+
res.status(200).send(data);
22+
});
23+
};
24+
beforeEach(() => {
25+
app = express();
26+
responseSizeMiddleware = new ResponseSizeMiddleware(responseMaxSizeInMb);
27+
app.use(responseSizeMiddleware.validateResponseSize());
28+
});
29+
30+
describe("validateResponseSize", () => {
31+
it("sends 200 when limit not exceeded", async () => {
32+
setUpRoute("test");
33+
supertest = request(app);
34+
await supertest.get(endpoint).expect((res) => {
35+
assert.strictEqual(res.status, 200);
36+
});
37+
});
38+
39+
it("sends 413 with message when response size is greate than max response size", async () => {
40+
const sizeInBytes = 5 * 1024 * 1024; // 5MB
41+
const largeObject = {
42+
data: "a".repeat(sizeInBytes),
43+
};
44+
setUpRoute(largeObject);
45+
supertest = request(app);
46+
await supertest.get(endpoint).expect((res) => {
47+
assert.strictEqual(res.status, 413);
48+
assert.strictEqual(res.body.error, "Response too large");
49+
assert.strictEqual(
50+
res.body.message,
51+
`Response size exceeds the maximum allowed size of ${responseMaxSizeInMb} megabytes`,
52+
);
53+
});
54+
});
55+
});
56+
});

0 commit comments

Comments
 (0)