Skip to content

Commit a06037d

Browse files
committed
refactor how Content-Type headers are checked
1 parent cbef6a6 commit a06037d

13 files changed

Lines changed: 81 additions & 34 deletions

File tree

controllers/utils.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* @author Claude Sonnet 4, cubap, thehabes
66
*/
77
import { newID, isValidID, db } from '../database/index.js'
8-
import utils from '../utils.js'
98

109
const ObjectID = newID
1110

rest.js

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,71 @@ const checkPatchOverrideSupport = function (req, res) {
2727
}
2828

2929
/**
30-
* Middleware to validate Content-Type headers.
31-
* Ensures that requests carrying bodies have an appropriate Content-Type before
32-
* reaching controllers, preventing unhandled errors from unparsed or mis-parsed bodies.
30+
* Middleware to validate JSON Content-Type headers for endpoints recieving JSON bodies.
3331
*
34-
* - Skips validation for methods that don't carry bodies (GET, HEAD, OPTIONS, DELETE)
35-
* - Skips validation for endpoints that don't carry bodies (/release)
36-
* - Allows text/plain for /search endpoints (which accept plain text search terms)
37-
* - Requires application/json or application/ld+json for all other write endpoints
38-
* - Returns 415 for missing or unsupported Content-Type
32+
* @param {Object} req - Express request object
33+
* @param {Object} res - Express response object
34+
* @param {Function} next - Express next middleware function
35+
*/
36+
const jsonContent = function (req, res, next) {
37+
const contentType = (req.get("Content-Type") ?? "").toLowerCase()
38+
const mimeType = contentType.split(";")[0].trim()
39+
if (!mimeType) {
40+
return next(createExpressError({
41+
statusCode: 415,
42+
statusMessage: `Missing or empty Content-Type header.`
43+
}))
44+
}
45+
if (contentType.includes(",")) {
46+
return next(createExpressError({
47+
statusCode: 415,
48+
statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.`
49+
}))
50+
}
51+
if (mimeType === "application/json" || mimeType === "application/ld+json") return next()
52+
return next(createExpressError({
53+
statusCode: 415,
54+
statusMessage: `Unsupported Content-Type: ${contentType}. This endpoint requires application/json or application/ld+json.`
55+
}))
56+
}
57+
58+
/**
59+
* Middleware to validate Content-Type headers for endpoints recieving textual bodies.
3960
*
4061
* @param {Object} req - Express request object
4162
* @param {Object} res - Express response object
4263
* @param {Function} next - Express next middleware function
4364
*/
44-
const SKIP_CONTENT_TYPE_METHODS = ["GET", "HEAD", "OPTIONS", "DELETE"]
45-
const validateContentType = function (req, res, next) {
46-
const isReleaseEndpoint = req.path === "/release" || req.path.startsWith("/release/")
47-
if (SKIP_CONTENT_TYPE_METHODS.includes(req.method) || isReleaseEndpoint) {
48-
return next()
65+
const textContent = function (req, res, next) {
66+
const contentType = (req.get("Content-Type") ?? "").toLowerCase()
67+
const mimeType = contentType.split(";")[0].trim()
68+
if (!mimeType) {
69+
return next(createExpressError({
70+
statusCode: 415,
71+
statusMessage: `Missing or empty Content-Type header.`
72+
}))
73+
}
74+
if (contentType.includes(",")) {
75+
return next(createExpressError({
76+
statusCode: 415,
77+
statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.`
78+
}))
4979
}
80+
if (mimeType === "text/plain") return next()
81+
return next(createExpressError({
82+
statusCode: 415,
83+
statusMessage: `Unsupported Content-Type: ${contentType}. This endpoint requires text/plain.`
84+
}))
85+
}
86+
87+
/**
88+
* Middleware to validate Content-Type headers for endpoints recieving either JSON or textual bodies.
89+
*
90+
* @param {Object} req - Express request object
91+
* @param {Object} res - Express response object
92+
* @param {Function} next - Express next middleware function
93+
*/
94+
const eitherContent = function (req, res, next) {
5095
const contentType = (req.get("Content-Type") ?? "").toLowerCase()
5196
const mimeType = contentType.split(";")[0].trim()
5297
if (!mimeType) {
@@ -61,13 +106,10 @@ const validateContentType = function (req, res, next) {
61106
statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.`
62107
}))
63108
}
64-
if (mimeType === "application/json" || mimeType === "application/ld+json") return next()
65-
const isSearchEndpoint = req.path === "/search" || req.path.startsWith("/search/")
66-
if (mimeType === "text/plain" && isSearchEndpoint) return next()
67-
const acceptedTypes = `application/json or application/ld+json${isSearchEndpoint ? ' or text/plain' : ''}`
109+
if (mimeType === "text/plain" || mimeType === "application/json" || mimeType === "application/ld+json") return next()
68110
return next(createExpressError({
69111
statusCode: 415,
70-
statusMessage: `Unsupported Content-Type: ${contentType}. This endpoint requires ${acceptedTypes}.`
112+
statusMessage: `Unsupported Content-Type: ${contentType}. This endpoint requires text/plain.`
71113
}))
72114
}
73115

@@ -163,4 +205,4 @@ It may not have completed at all, and most likely did not complete successfully.
163205
res.status(error.status).send(error.message)
164206
}
165207

166-
export default { checkPatchOverrideSupport, validateContentType, messenger }
208+
export default { checkPatchOverrideSupport, jsonContent, textContent, messenger }

routes/api-routes.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,10 @@ import releaseRouter from './release.js';
4444
import sinceRouter from './since.js';
4545
// Support GET requests like v1/history/{object id} to discover all previous versions tracing back to the prime.
4646
import historyRouter from './history.js';
47-
import rest from '../rest.js'
4847

4948
router.use(staticRouter)
50-
router.use('/id',idRouter)
49+
router.use('/id', idRouter)
5150
router.use('/api', compatabilityRouter)
52-
router.use('/api', rest.validateContentType)
5351
router.use('/api/query', queryRouter)
5452
router.use('/api/search', searchRouter)
5553
router.use('/api/create', createRouter)

routes/bulkCreate.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ const router = express.Router()
55
//This controller will handle all MongoDB interactions.
66
import controller from '../db-controller.js'
77
import auth from '../auth/index.js'
8+
import { jsonContent } from '../rest.js'
89

910
router.route('/')
10-
.post(auth.checkJwt, controller.bulkCreate)
11+
.post(auth.checkJwt, jsonContent, controller.bulkCreate)
1112
.all((req, res, next) => {
1213
res.statusMessage = 'Improper request method for creating, please use POST.'
1314
res.status(405)

routes/bulkUpdate.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ const router = express.Router()
55
//This controller will handle all MongoDB interactions.
66
import controller from '../db-controller.js'
77
import auth from '../auth/index.js'
8+
import { jsonContent } from '../rest.js'
89

910
router.route('/')
10-
.put(auth.checkJwt, controller.bulkUpdate)
11+
.put(auth.checkJwt, jsonContent, controller.bulkUpdate)
1112
.all((req, res, next) => {
1213
res.statusMessage = 'Improper request method for creating, please use PUT.'
1314
res.status(405)

routes/create.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ const router = express.Router()
44
//This controller will handle all MongoDB interactions.
55
import controller from '../db-controller.js'
66
import auth from '../auth/index.js'
7+
import { jsonContent } from '../rest.js'
78

89
router.route('/')
9-
.post(auth.checkJwt, controller.create)
10+
.post(auth.checkJwt, jsonContent, controller.create)
1011
.all((req, res, next) => {
1112
res.statusMessage = 'Improper request method for creating, please use POST.'
1213
res.status(405)

routes/overwrite.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ const router = express.Router()
44
//This controller will handle all MongoDB interactions.
55
import controller from '../db-controller.js'
66
import auth from '../auth/index.js'
7+
import { jsonContent } from '../rest.js'
78

89
router.route('/')
9-
.put(auth.checkJwt, controller.overwrite)
10+
.put(auth.checkJwt, jsonContent, controller.overwrite)
1011
.all((req, res, next) => {
1112
res.statusMessage = 'Improper request method for overwriting, please use PUT to overwrite this object.'
1213
res.status(405)

routes/patchSet.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ const router = express.Router()
44
import controller from '../db-controller.js'
55
import auth from '../auth/index.js'
66
import rest from '../rest.js'
7+
import { jsonContent } from '../rest.js'
78

89
router.route('/')
9-
.patch(auth.checkJwt, controller.patchSet)
10+
.patch(auth.checkJwt, jsonContent, controller.patchSet)
1011
.post(auth.checkJwt, (req, res, next) => {
1112
if (rest.checkPatchOverrideSupport(req, res)) {
1213
controller.patchSet(req, res, next)

routes/patchUnset.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import auth from '../auth/index.js'
66
import rest from '../rest.js'
77

88
router.route('/')
9-
.patch(auth.checkJwt, controller.patchUnset)
9+
.patch(auth.checkJwt, rest.jsonContent, controller.patchUnset)
1010
.post(auth.checkJwt, (req, res, next) => {
1111
if (rest.checkPatchOverrideSupport(req, res)) {
1212
controller.patchUnset(req, res, next)

routes/patchUpdate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import rest from '../rest.js'
77
import auth from '../auth/index.js'
88

99
router.route('/')
10-
.patch(auth.checkJwt, controller.patchUpdate)
10+
.patch(auth.checkJwt, rest.jsonContent, controller.patchUpdate)
1111
.post(auth.checkJwt, (req, res, next) => {
1212
if (rest.checkPatchOverrideSupport(req, res)) {
1313
controller.patchUpdate(req, res, next)

0 commit comments

Comments
 (0)