Skip to content

Commit 194dd41

Browse files
authored
Merge pull request #179 from TerriaJS/share-url-too-large
Set 413 response status when share data is too large
2 parents 8f81c8d + ea2862f commit 194dd41

File tree

6 files changed

+260
-111
lines changed

6 files changed

+260
-111
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
### Next version
22

3+
* Exposed and the 413 status code for the `/share` endpoint to the client for a more meaningful error handling when the story causes shareData to exceed the limit.
4+
* Exposed the `shareMaxRequestSize` string value and `shareMaxRequestSizeBytes` numeric value in the `/serverconfig` endpoint.
5+
36
### 4.0.2 - 2025-06-03
47

58
* Requires Node 20.

lib/controllers/serverconfig.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* jshint node: true, esnext: true */
22
"use strict";
33
var express = require('express');
4+
var bytes = require('bytes');
45

56
// Expose a whitelisted set of configuration attributes to the world. This definitely doesn't include authorisation tokens, local file paths, etc.
67
// It mirrors the structure of the real config file.
78
module.exports = function(options) {
89
var router = express.Router();
910
var settings = Object.assign({}, options.settings), safeSettings = {};
10-
var safeAttributes = ['allowProxyFor', 'newShareUrlPrefix', 'proxyAllDomains'];
11+
var safeAttributes = ['allowProxyFor', 'newShareUrlPrefix', 'proxyAllDomains', 'shareMaxRequestSize'];
1112
safeAttributes.forEach(key => safeSettings[key] = settings[key]);
1213
safeSettings.version = require('../../package.json').version;
1314
if (typeof settings.shareUrlPrefixes === 'object') {
@@ -19,6 +20,9 @@ module.exports = function(options) {
1920
if (settings.feedback && settings.feedback.additionalParameters) {
2021
safeSettings.additionalFeedbackParameters = settings.feedback.additionalParameters;
2122
}
23+
if (settings.shareMaxRequestSize) {
24+
safeSettings.shareMaxRequestSizeBytes = bytes.parse(settings.shareMaxRequestSize);
25+
}
2226

2327
router.get('/', function(req, res, next) {
2428
res.status(200).send(safeSettings);

lib/controllers/share.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ module.exports = function(hostName, port, options) {
148148
limit: options.shareMaxRequestSize || '200kb'
149149
}));
150150

151+
// Return the 413 error thrown by body-parser to the client
152+
router.use(function(error, req, res, next) {
153+
if (error && (error.status === 413 || error.type === 'entity.too.large')) {
154+
res.status(413).send('Payload Too Large');
155+
} else {
156+
next(error);
157+
}
158+
});
159+
151160
// Requested creation of a new short URL.
152161
router.post('/', function(req, res, next) {
153162
if (options.newShareUrlPrefix === undefined || !options.shareUrlPrefixes[options.newShareUrlPrefix]) {

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"base-x": "^5.0.1",
3434
"basic-auth": "^2.0.1",
3535
"body-parser": "^1.20.3",
36+
"bytes": "^3.1.2",
3637
"compression": "^1.6.0",
3738
"cors": "^2.7.1",
3839
"express": "^4.21.2",
@@ -48,6 +49,7 @@
4849
},
4950
"devDependencies": {
5051
"jasmine": "^4.6.0",
52+
"nock": "^14.0.4",
5153
"supertest": "^7.0.0"
5254
}
5355
}

spec/share.spec.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"use strict";
2+
3+
const makeServer = require('../lib/makeserver');
4+
const request = require('supertest');
5+
const nock = require('nock');
6+
const path = require('path');
7+
8+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
9+
10+
describe('share endpoint (integration, real controller)', function() {
11+
const appOptions = {
12+
wwwroot: "./spec/mockwwwroot",
13+
hostName: "localhost",
14+
port: "3001",
15+
settings: {
16+
shareUrlPrefixes: {
17+
"g": {
18+
"service": "gist",
19+
"userAgent": "TerriaJS-Server",
20+
"gistFilename": "terriajs-server-catalog.json",
21+
"gistDescription": "TerriaJS Shared catalog"
22+
}
23+
},
24+
newShareUrlPrefix: "g",
25+
shareMaxRequestSize: "200kb"
26+
}
27+
};
28+
29+
function buildApp() {
30+
const options = require('../lib/options').init(true);
31+
const mergedOptions = Object.assign(options, appOptions);
32+
return makeServer(mergedOptions);
33+
}
34+
35+
beforeEach(() => {
36+
nock.cleanAll();
37+
});
38+
39+
afterAll(() => {
40+
nock.restore();
41+
});
42+
43+
describe('POST /share', function() {
44+
it('should return 201 and correct response for valid payload', function(done) {
45+
// Mock GitHub Gist API
46+
const fakeGistId = "123456";
47+
nock('https://api.github.com')
48+
.post('/gists')
49+
.reply(201, {
50+
id: fakeGistId
51+
});
52+
53+
const payload = { test: "me" };
54+
55+
request(buildApp())
56+
.post('/share')
57+
.send(payload)
58+
.expect(201)
59+
.expect('Content-Type', /json/)
60+
.expect(res => {
61+
const actualUrl = res.body.url;
62+
const expectedPath = `/share/g-${fakeGistId}`;
63+
expect(res.body.id).toBe(`g-${fakeGistId}`);
64+
expect(res.body.path).toBe(expectedPath);
65+
expect(actualUrl.endsWith(expectedPath)).toBeTrue();
66+
})
67+
.end(function(err) {
68+
if (err) return done.fail(err);
69+
done();
70+
});
71+
});
72+
73+
it('should return 413 when payload exceeds shareMaxRequestSize', function(done) {
74+
const largePayload = 'a'.repeat(250000); // 250KB
75+
request(buildApp())
76+
.post('/share')
77+
.set('Content-Type', 'application/json')
78+
.send(`{"data":"${largePayload}"}`)
79+
.expect(413)
80+
.expect('Content-Type', /text|plain/)
81+
.expect(res => {
82+
expect(
83+
res.text.includes('Payload Too Large')
84+
).toBeTrue();
85+
})
86+
.end(function(err) {
87+
if (err) return done.fail(err);
88+
done();
89+
});
90+
});
91+
});
92+
});

0 commit comments

Comments
 (0)