mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): redirect on error if request ACCEPTs html (#56445)
This commit is contained in:
committed by
GitHub
parent
250f313dad
commit
cb4061c250
+6
-25
@@ -1,8 +1,8 @@
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
import fastifyCsrfProtection from '@fastify/csrf-protection';
|
||||
import fastifySwagger from '@fastify/swagger';
|
||||
import fastifySwaggerUI from '@fastify/swagger-ui';
|
||||
import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
|
||||
import fastifySentry from '@immobiliarelabs/fastify-sentry';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import uriResolver from 'fast-uri';
|
||||
@@ -25,6 +25,7 @@ import redirectWithMessage from './plugins/redirect-with-message';
|
||||
import security from './plugins/security';
|
||||
import auth from './plugins/auth';
|
||||
import bouncer from './plugins/bouncer';
|
||||
import errorHandling from './plugins/error-handling';
|
||||
import notFound from './plugins/not-found';
|
||||
import * as publicRoutes from './routes/public';
|
||||
import * as protectedRoutes from './routes/protected';
|
||||
@@ -33,8 +34,7 @@ import {
|
||||
API_LOCATION,
|
||||
EMAIL_PROVIDER,
|
||||
FCC_ENABLE_DEV_LOGIN_MODE,
|
||||
FCC_ENABLE_SWAGGER_UI,
|
||||
SENTRY_DSN
|
||||
FCC_ENABLE_SWAGGER_UI
|
||||
} from './utils/env';
|
||||
import { isObjectID } from './utils/validation';
|
||||
|
||||
@@ -81,27 +81,10 @@ export const build = async (
|
||||
|
||||
fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema));
|
||||
|
||||
void fastify.register(redirectWithMessage);
|
||||
void fastify.register(security);
|
||||
|
||||
await fastify.register(fastifySentry, {
|
||||
dsn: SENTRY_DSN,
|
||||
// No need to initialize if DSN is not provided (e.g. in development and
|
||||
// test environments)
|
||||
skipInit: !SENTRY_DSN,
|
||||
errorResponse: (error, _request, reply) => {
|
||||
const isCSRFError =
|
||||
error.code === 'FST_CSRF_INVALID_TOKEN' ||
|
||||
error.code === 'FST_CSRF_MISSING_SECRET';
|
||||
if (reply.statusCode === 500 || isCSRFError) {
|
||||
void reply.send({
|
||||
message: 'flash.generic-error',
|
||||
type: 'danger'
|
||||
});
|
||||
} else {
|
||||
void reply.send(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
void fastify.register(fastifyAccepts);
|
||||
void fastify.register(errorHandling);
|
||||
|
||||
await fastify.register(cors);
|
||||
await fastify.register(cookies);
|
||||
@@ -165,8 +148,6 @@ export const build = async (
|
||||
fastify.log.info(`Swagger UI available at ${API_LOCATION}/documentation`);
|
||||
}
|
||||
|
||||
// redirectWithMessage must be registered before codeFlowAuth
|
||||
void fastify.register(redirectWithMessage);
|
||||
void fastify.register(auth);
|
||||
void fastify.register(notFound);
|
||||
void fastify.register(prismaPlugin);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import accepts from '@fastify/accepts';
|
||||
|
||||
import errorHandling from './error-handling';
|
||||
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
||||
|
||||
describe('errorHandling', () => {
|
||||
let fastify: FastifyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
fastify = Fastify();
|
||||
await fastify.register(redirectWithMessage);
|
||||
await fastify.register(accepts);
|
||||
await fastify.register(errorHandling);
|
||||
|
||||
fastify.get('/test', async (_req, _reply) => {
|
||||
throw new Error('a very bad thing happened');
|
||||
return { ok: true };
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastify.close();
|
||||
});
|
||||
|
||||
it('should redirect to the referer if the request does not Accept json', async () => {
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
headers: {
|
||||
referer: 'https://www.freecodecamp.org/anything',
|
||||
accept: 'text/plain'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.headers['location']).toEqual(
|
||||
'https://www.freecodecamp.org/anything?' +
|
||||
formatMessage({
|
||||
type: 'danger',
|
||||
content: 'flash.generic-error'
|
||||
})
|
||||
);
|
||||
expect(res.statusCode).toEqual(302);
|
||||
});
|
||||
|
||||
it('should return a json response if the request does Accept json', async () => {
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
headers: {
|
||||
referer: 'https://www.freecodecamp.org/anything',
|
||||
accept: 'application/json,text/plain'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.json()).toEqual({
|
||||
message: 'flash.generic-error',
|
||||
type: 'danger'
|
||||
});
|
||||
expect(res.statusCode).toEqual(500);
|
||||
});
|
||||
|
||||
it('should redirect if the request prefers text/html to json', async () => {
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
headers: {
|
||||
referer: 'https://www.freecodecamp.org/anything',
|
||||
// this does accept json, (via the */*), but prefers text/html
|
||||
accept: 'text/html,*/*'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.headers['location']).toEqual(
|
||||
'https://www.freecodecamp.org/anything?' +
|
||||
formatMessage({
|
||||
type: 'danger',
|
||||
content: 'flash.generic-error'
|
||||
})
|
||||
);
|
||||
expect(res.statusCode).toEqual(302);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import fastifySentry from '@immobiliarelabs/fastify-sentry';
|
||||
import fp from 'fastify-plugin';
|
||||
|
||||
import { SENTRY_DSN } from '../utils/env';
|
||||
import { getRedirectParams } from '../utils/redirection';
|
||||
|
||||
/**
|
||||
* Plugin to handle errors and send them to Sentry.
|
||||
*
|
||||
* @param fastify The Fastify instance.
|
||||
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`.
|
||||
* @param done Callback to signal that the logic has completed.
|
||||
*/
|
||||
const errorHandling: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
void fastify.register(fastifySentry, {
|
||||
dsn: SENTRY_DSN,
|
||||
// No need to initialize if DSN is not provided (e.g. in development and
|
||||
// test environments)
|
||||
skipInit: !SENTRY_DSN,
|
||||
errorResponse: (error, request, reply) => {
|
||||
const accepts = request.accepts().type(['json', 'html']);
|
||||
const isCSRFError =
|
||||
error.code === 'FST_CSRF_INVALID_TOKEN' ||
|
||||
error.code === 'FST_CSRF_MISSING_SECRET';
|
||||
|
||||
const { returnTo } = getRedirectParams(request);
|
||||
|
||||
const message =
|
||||
reply.statusCode === 500 || isCSRFError
|
||||
? 'flash.generic-error'
|
||||
: error.message;
|
||||
if (accepts === 'json') {
|
||||
void reply.send({
|
||||
message,
|
||||
type: 'danger'
|
||||
});
|
||||
} else {
|
||||
void reply.status(302);
|
||||
void reply.redirectWithMessage(returnTo, {
|
||||
type: 'danger',
|
||||
content: message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
export default fp(errorHandling, {
|
||||
dependencies: ['redirect-with-message', '@fastify/accepts']
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import accepts from '@fastify/accepts';
|
||||
|
||||
import notFound from './not-found';
|
||||
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
||||
@@ -9,6 +10,7 @@ describe('fourOhFour', () => {
|
||||
beforeEach(async () => {
|
||||
fastify = Fastify();
|
||||
await fastify.register(redirectWithMessage);
|
||||
await fastify.register(accepts);
|
||||
await fastify.register(notFound);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
import fp from 'fastify-plugin';
|
||||
import accepts from '@fastify/accepts';
|
||||
|
||||
import { getRedirectParams } from '../utils/redirection';
|
||||
|
||||
@@ -13,8 +12,6 @@ import { getRedirectParams } from '../utils/redirection';
|
||||
* @param done Callback to signal that the logic has completed.
|
||||
*/
|
||||
const fourOhFour: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
void fastify.register(accepts);
|
||||
|
||||
// If the request accepts JSON and does not specifically prefer text/html,
|
||||
// this will return a 404 JSON response. Everything else will be redirected.
|
||||
fastify.setNotFoundHandler((request, reply) => {
|
||||
@@ -34,4 +31,6 @@ const fourOhFour: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
done();
|
||||
};
|
||||
|
||||
export default fp(fourOhFour, { dependencies: ['redirect-with-message'] });
|
||||
export default fp(fourOhFour, {
|
||||
dependencies: ['redirect-with-message', '@fastify/accepts']
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user