Skip to content

Commit

Permalink
feat(server): added timeout middleware (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh authored Aug 31, 2024
1 parent f75c142 commit 3f53640
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ You can configure the application using environment variables. Here are the avai
| Environment Variable | Description | Default Value |
| -------------------- | ----------- | ------------- |
| `PORT` | The port to listen on when using node server | `8787` |
| `SERVER_API_ROUTES_TIMEOUT_MS` | The maximum time in milliseconds for a route to complete before timing out | `5000` |
| `SERVER_CORS_ORIGINS` | The CORS origin for the api server | _No default value_ |
| `NOTES_MAX_ENCRYPTED_CONTENT_LENGTH` | The maximum length of the encrypted content of a note allowed by the api | `5242880` |
| `TASK_DELETE_EXPIRED_NOTES_ENABLED` | Whether to enable a periodic task to delete expired notes (not available for cloudflare) | `true` |
Expand Down
1 change: 0 additions & 1 deletion packages/app-server/.nvmrc

This file was deleted.

8 changes: 7 additions & 1 deletion packages/app-server/src/modules/app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export const configDefinition = {
default: 8787,
env: 'PORT',
},
routeTimeoutMs: {
doc: 'The maximum time in milliseconds for a route to complete before timing out',
schema: z.coerce.number().int().positive(),
default: 5_000,
env: 'SERVER_API_ROUTES_TIMEOUT_MS',
},
corsOrigins: {
doc: 'The CORS origin for the api server',
schema: z.union([
Expand All @@ -28,7 +34,7 @@ export const configDefinition = {
notes: {
maxEncryptedContentLength: {
doc: 'The maximum length of the encrypted content of a note allowed by the api',
schema: z.number().min(1),
schema: z.coerce.number().int().positive().min(1),
default: 1024 * 1024 * 5, // 5MB
env: 'NOTES_MAX_ENCRYPTED_CONTENT_LENGTH',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, test } from 'vitest';
import { Hono } from 'hono';
import { timeoutMiddleware } from './timeout.middleware';
import { registerErrorMiddleware } from './errors.middleware';

describe('middlewares', () => {
describe('timeoutMiddleware', () => {
test('when a request last longer than the config timeout, a 504 error is raised', async () => {
const app = new Hono<{ Variables: { config: any } }>();
registerErrorMiddleware({ app: app as any });

app.use(async (context, next) => {
context.set('config', { server: { routeTimeoutMs: 50 } });

await next();
});

app.get(
'/should-timeout',
timeoutMiddleware,
async (context) => {
await new Promise(resolve => setTimeout(resolve, 100));
return context.json({ status: 'ok' });
},
);

app.get(
'/should-not-timeout',
timeoutMiddleware,
async (context) => {
return context.json({ status: 'ok' });
},
);

const response1 = await app.request('/should-timeout', { method: 'GET' });

expect(response1.status).to.eql(504);
expect(await response1.json()).to.eql({
error: {
code: 'api.timeout',
message: 'The request timed out',
},
});

const response2 = await app.request('/should-not-timeout', { method: 'GET' });

expect(response2.status).to.eql(200);
expect(await response2.json()).to.eql({ status: 'ok' });
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createMiddleware } from 'hono/factory';
import type { Context } from '../server.types';
import { createError } from '../../shared/errors/errors';

export const timeoutMiddleware = createMiddleware(async (context: Context, next) => {
const { server: { routeTimeoutMs } } = context.get('config');

let timerId: NodeJS.Timeout | undefined;

const timeoutPromise = new Promise((_, reject) => {
timerId = setTimeout(() => reject(
createError({
code: 'api.timeout',
message: 'The request timed out',
statusCode: 504,
}),
), routeTimeoutMs);
});

try {
await Promise.race([next(), timeoutPromise]);
} finally {
if (timerId !== undefined) {
clearTimeout(timerId);
}
}
});
2 changes: 2 additions & 0 deletions packages/app-server/src/modules/app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { loggerMiddleware } from './middlewares/logger.middleware';
import { registerErrorMiddleware } from './middlewares/errors.middleware';
import { createStorageMiddleware } from './middlewares/storage.middleware';
import type { Config } from './config/config.types';
import { timeoutMiddleware } from './middlewares/timeout.middleware';

export { createServer };

Expand All @@ -17,6 +18,7 @@ function createServer({ config, storageFactory }: { config?: Config; storageFact

app.use(loggerMiddleware);
app.use(createConfigMiddleware({ config }));
app.use(timeoutMiddleware);
app.use(corsMiddleware);
app.use(createStorageMiddleware({ storageFactory }));
app.use(secureHeaders());
Expand Down

0 comments on commit 3f53640

Please sign in to comment.