From 176e0a195011030624be338e95dc5a346b4ee52b Mon Sep 17 00:00:00 2001 From: darkskygit Date: Tue, 18 Feb 2025 11:34:58 +0000 Subject: [PATCH] fix: raw body limit (#10254) --- .../server/src/__tests__/app/graphql.e2e.ts | 52 +++++++++++++++++++ .../server/src/__tests__/app/selfhost.e2e.ts | 25 +++++++++ .../server/src/__tests__/utils/testing-app.ts | 6 ++- packages/backend/server/src/app.ts | 7 +-- .../doc-renderer/__tests__/controller.spec.ts | 2 +- 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packages/backend/server/src/__tests__/app/graphql.e2e.ts b/packages/backend/server/src/__tests__/app/graphql.e2e.ts index 117d426662..e6916940aa 100644 --- a/packages/backend/server/src/__tests__/app/graphql.e2e.ts +++ b/packages/backend/server/src/__tests__/app/graphql.e2e.ts @@ -1,8 +1,13 @@ +import { Args, Mutation, Resolver } from '@nestjs/graphql'; import type { TestFn } from 'ava'; import ava from 'ava'; +import GraphQLUpload, { + type FileUpload, +} from 'graphql-upload/GraphQLUpload.mjs'; import request from 'supertest'; import { buildAppModule } from '../../app.module'; +import { Public } from '../../core/auth'; import { createTestingApp, TestingApp } from '../utils'; const gql = '/graphql'; @@ -11,6 +16,26 @@ const test = ava as TestFn<{ app: TestingApp; }>; +@Resolver(() => String) +class TestResolver { + @Public() + @Mutation(() => Number) + async upload( + @Args({ name: 'body', type: () => GraphQLUpload }) + body: FileUpload + ): Promise { + const size = await new Promise((resolve, reject) => { + const stream = body.createReadStream(); + let size = 0; + stream.on('data', chunk => (size += chunk.length)); + stream.on('error', reject); + stream.on('end', () => resolve(size)); + }); + + return size; + } +} + test.before('start app', async t => { // @ts-expect-error override AFFiNE.flavor = { @@ -19,6 +44,7 @@ test.before('start app', async t => { } as typeof AFFiNE.flavor; const app = await createTestingApp({ imports: [buildAppModule()], + providers: [TestResolver], }); t.context.app = app; @@ -83,3 +109,29 @@ test('should not throw internal error when graphql call with invalid params', as message: /Failed to execute gql: query { workspace\("1"\) \}, status: 400/, }); }); + +test('should can send maximum size of body', async t => { + const { app } = t.context; + + const body = Buffer.from('a'.repeat(10 * 1024 * 1024 - 1)); + const res = await app + .POST('/graphql') + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .field( + 'operations', + JSON.stringify({ + name: 'upload', + query: `mutation upload($body: Upload!) { upload(body: $body) }`, + variables: { body: null }, + }) + ) + .field('map', JSON.stringify({ '0': ['variables.body'] })) + .attach( + '0', + body, + `body-${Math.random().toString(16).substring(2, 10)}.data` + ) + .expect(200); + + t.is(Number(res.body.data.upload), body.length); +}); diff --git a/packages/backend/server/src/__tests__/app/selfhost.e2e.ts b/packages/backend/server/src/__tests__/app/selfhost.e2e.ts index 28142c86f2..f894b7e60f 100644 --- a/packages/backend/server/src/__tests__/app/selfhost.e2e.ts +++ b/packages/backend/server/src/__tests__/app/selfhost.e2e.ts @@ -1,6 +1,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; +import { Controller, Post, RawBody } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import type { TestFn } from 'ava'; import ava from 'ava'; @@ -8,6 +9,7 @@ import request from 'supertest'; import { buildAppModule } from '../../app.module'; import { Config } from '../../base'; +import { Public } from '../../core/auth'; import { ServerService } from '../../core/config'; import { createTestingApp, type TestingApp } from '../utils'; @@ -36,12 +38,22 @@ function initTestStaticFiles(staticPath: string) { } } +@Controller('/') +export class TestResolver { + @Public() + @Post('/upload') + async upload(@RawBody() buffer: Buffer | undefined): Promise { + return buffer?.length || 0; + } +} + test.before('init selfhost server', async t => { // @ts-expect-error override AFFiNE.isSelfhosted = true; AFFiNE.flavor.renderer = true; const app = await createTestingApp({ imports: [buildAppModule()], + controllers: [TestResolver], }); t.context.app = app; @@ -203,3 +215,16 @@ test.skip('should return web assets if visited by mobile', async t => { t.true(res.text.includes('AFFiNE mobile')); }); + +test('should can send maximum size of body', async t => { + const { app } = t.context; + + const body = 'a'.repeat(1 * 1024 * 1024); + const res = await app + .POST('/upload') + .set('Content-Type', 'application/octet-stream') + .send(body) + .expect(201); + + t.is(Number(res.text), body.length); +}); diff --git a/packages/backend/server/src/__tests__/utils/testing-app.ts b/packages/backend/server/src/__tests__/utils/testing-app.ts index 791226467c..80d4713018 100644 --- a/packages/backend/server/src/__tests__/utils/testing-app.ts +++ b/packages/backend/server/src/__tests__/utils/testing-app.ts @@ -19,6 +19,8 @@ interface TestingAppMetadata extends ModuleMetadata { export type TestUser = Omit & { password: string }; +const OneMB = 1024 * 1024; + export async function createTestingApp( moduleDef: TestingAppMetadata = {} ): Promise { @@ -30,7 +32,7 @@ export async function createTestingApp( rawBody: true, }); - app.useBodyParser('raw'); + app.useBodyParser('raw', { limit: 1 * OneMB }); const logger = new AFFiNELogger(); @@ -40,7 +42,7 @@ export async function createTestingApp( app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter())); app.use( graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, + maxFileSize: 10 * OneMB, maxFiles: 5, }) ); diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index d221986887..cad0d93974 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -14,6 +14,8 @@ import { AuthGuard } from './core/auth'; import { ENABLED_FEATURES } from './core/config/server-feature'; import { serverTimingAndCache } from './middleware/timing'; +const OneMB = 1024 * 1024; + export async function createApp() { const { AppModule } = await import('./app.module'); @@ -24,7 +26,7 @@ export async function createApp() { bufferLogs: true, }); - app.useBodyParser('raw', { limit: '100mb' }); + app.useBodyParser('raw', { limit: 100 * OneMB }); app.useLogger(app.get(AFFiNELogger)); @@ -36,8 +38,7 @@ export async function createApp() { app.use( graphqlUploadExpress({ - // TODO(@darkskygit): dynamic limit by quota maybe? - maxFileSize: 100 * 1024 * 1024, + maxFileSize: 100 * OneMB, maxFiles: 32, }) ); diff --git a/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts b/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts index e05089e9b1..a0b2d1d5de 100644 --- a/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts +++ b/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts @@ -58,7 +58,7 @@ test.after.always(async t => { await t.context.app.close(); }); -test.only('should render page success', async t => { +test('should render page success', async t => { const docId = randomUUID(); const { app, adapter, permission } = t.context;