Skip to content

Commit e62d060

Browse files
authored
Allow trailing slashes (#82)
* Add failing tests for trailing slash requests * Update routes to allow trailing slashes * Allow health endpoint to also accept trailing slash * Whitelist endpoint is not an async method
1 parent e625736 commit e62d060

10 files changed

+351
-239
lines changed

src/routes/index.js

+48-23
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,62 @@
11
import cache from '../utils/cache';
22

3+
/**
4+
* Handle GET / requests.
5+
*
6+
* @param {import('hono').Context} ctx Request context.
7+
* @return {Response}
8+
*/
9+
const handleGet = ctx => {
10+
// Set a 355 day (same as CDN) life on this response
11+
// This is also immutable
12+
cache(ctx, 355 * 24 * 60 * 60, true);
13+
14+
// Redirect to the API docs
15+
return ctx.redirect('https://cdnjs.com/api', 301);
16+
};
17+
18+
/**
19+
* Handle GET /health requests.
20+
*
21+
* @param {import('hono').Context} ctx Request context.
22+
* @return {Response}
23+
*/
24+
const handleGetHealth = ctx => {
25+
// Don't cache health, ensure its always live
26+
cache(ctx, -1);
27+
28+
// Respond
29+
return ctx.text('OK');
30+
};
31+
32+
/**
33+
* Handle GET /robots.txt requests.
34+
*
35+
* @param {import('hono').Context} ctx Request context.
36+
* @return {Response}
37+
*/
38+
const handleGetRobotsTxt = ctx => {
39+
// Set a 355 day (same as CDN) life on this response
40+
// This is also immutable
41+
cache(ctx, 355 * 24 * 60 * 60, true);
42+
43+
// Disallow all robots
44+
return ctx.text('User-agent: *\nDisallow: /');
45+
};
46+
347
/**
448
* Register core routes.
549
*
650
* @param {import('hono').Hono} app App instance.
751
*/
852
export default app => {
953
// Redirect root the API docs
10-
app.get('/', ctx => {
11-
// Set a 355 day (same as CDN) life on this response
12-
// This is also immutable
13-
cache(ctx, 355 * 24 * 60 * 60, true);
14-
15-
// Redirect to the API docs
16-
return ctx.redirect('https://cdnjs.com/api', 301);
17-
});
54+
app.get('/', handleGet);
1855

1956
// Respond that the API is up
20-
app.get('/health', ctx => {
21-
// Don't cache health, ensure its always live
22-
cache(ctx, -1);
23-
24-
// Respond
25-
return ctx.text('OK');
26-
});
57+
app.get('/health', handleGetHealth);
58+
app.get('/health/', handleGetHealth);
2759

2860
// Don't ever index anything on the API
29-
app.get('/robots.txt', ctx => {
30-
// Set a 355 day (same as CDN) life on this response
31-
// This is also immutable
32-
cache(ctx, 355 * 24 * 60 * 60, true);
33-
34-
// Disallow all robots
35-
return ctx.text('User-agent: *\nDisallow: /');
36-
});
61+
app.get('/robots.txt', handleGetRobotsTxt);
3762
};

src/routes/index.spec.js

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ describe('/health', () => {
4444
expect(response).to.be.text;
4545
expect(response.text).to.eq('OK');
4646
});
47+
48+
// Test with a trailing slash
49+
it('responds to requests with a trailing slash', async () => {
50+
const res = await request(path + '/');
51+
expect(res).to.have.status(200);
52+
expect(res.body).to.deep.equal(response.body);
53+
});
4754
});
4855

4956
describe('/robots.txt', () => {

src/routes/libraries.js

+59-50
Original file line numberDiff line numberDiff line change
@@ -61,61 +61,70 @@ const browse = async (query, searchFields) => {
6161
};
6262

6363
/**
64-
* Register libraries routes.
64+
* Handle GET /libraries requests.
6565
*
66-
* @param {import('hono').Hono} app App instance.
66+
* @param {import('hono').Context} ctx Request context.
67+
* @return {Promise<Response>}
6768
*/
68-
export default app => {
69-
app.get('/libraries', async ctx => {
70-
// Get the index results
71-
const searchFields = queryArray(ctx.req.queries('search_fields'));
72-
const results = await browse(
73-
(ctx.req.query('search') || '').toString().slice(0, maxQueryLength),
74-
searchFields.includes('*') ? [] : searchFields,
75-
);
69+
const handleGetLibraries = async ctx => {
70+
// Get the index results
71+
const searchFields = queryArray(ctx.req.queries('search_fields'));
72+
const results = await browse(
73+
(ctx.req.query('search') || '').toString().slice(0, maxQueryLength),
74+
searchFields.includes('*') ? [] : searchFields,
75+
);
7676

77-
// Transform the results into our filtered array
78-
const requestedFields = queryArray(ctx.req.queries('fields'));
79-
const response = results.filter(hit => {
80-
if (hit?.name) return true;
81-
console.warn('Found bad entry in Algolia data');
82-
console.info(hit);
83-
ctx.sentry?.withScope(scope => {
84-
scope.setExtra('hit', hit);
85-
ctx.sentry.captureException(new Error('Bad entry in Algolia data'));
86-
});
87-
return false;
88-
}).map(hit => filter(
89-
{
90-
// Ensure name is first prop
91-
name: hit.name,
92-
// Custom latest prop
93-
latest: hit.filename && hit.version ? 'https://cdnjs.cloudflare.com/ajax/libs/' + hit.name + '/' + hit.version + '/' + hit.filename : null,
94-
// All other hit props
95-
...hit,
96-
},
97-
[
98-
// Always send back name & latest
99-
'name',
100-
'latest',
101-
// Send back whatever else was requested
102-
...requestedFields,
103-
],
104-
requestedFields.includes('*'), // Send all if they have '*'
105-
));
77+
// Transform the results into our filtered array
78+
const requestedFields = queryArray(ctx.req.queries('fields'));
79+
const response = results.filter(hit => {
80+
if (hit?.name) return true;
81+
console.warn('Found bad entry in Algolia data');
82+
console.info(hit);
83+
ctx.sentry?.withScope(scope => {
84+
scope.setExtra('hit', hit);
85+
ctx.sentry.captureException(new Error('Bad entry in Algolia data'));
86+
});
87+
return false;
88+
}).map(hit => filter(
89+
{
90+
// Ensure name is first prop
91+
name: hit.name,
92+
// Custom latest prop
93+
latest: hit.filename && hit.version ? 'https://cdnjs.cloudflare.com/ajax/libs/' + hit.name + '/' + hit.version + '/' + hit.filename : null,
94+
// All other hit props
95+
...hit,
96+
},
97+
[
98+
// Always send back name & latest
99+
'name',
100+
'latest',
101+
// Send back whatever else was requested
102+
...requestedFields,
103+
],
104+
requestedFields.includes('*'), // Send all if they have '*'
105+
));
106106

107-
// If they want less data, allow that
108-
const limit = ctx.req.query('limit') && Number(ctx.req.query('limit'));
109-
const trimmed = limit ? response.slice(0, limit) : response;
107+
// If they want less data, allow that
108+
const limit = ctx.req.query('limit') && Number(ctx.req.query('limit'));
109+
const trimmed = limit ? response.slice(0, limit) : response;
110110

111-
// Set a 6 hour life on this response
112-
cache(ctx, 6 * 60 * 60);
111+
// Set a 6 hour life on this response
112+
cache(ctx, 6 * 60 * 60);
113113

114-
// Send the response
115-
return respond(ctx, {
116-
results: trimmed,
117-
total: trimmed.length, // Total results we're sending back
118-
available: response.length, // Total number available without trimming
119-
});
114+
// Send the response
115+
return respond(ctx, {
116+
results: trimmed,
117+
total: trimmed.length, // Total results we're sending back
118+
available: response.length, // Total number available without trimming
120119
});
121120
};
121+
122+
/**
123+
* Register libraries routes.
124+
*
125+
* @param {import('hono').Hono} app App instance.
126+
*/
127+
export default app => {
128+
app.get('/libraries', handleGetLibraries);
129+
app.get('/libraries/', handleGetLibraries);
130+
};

src/routes/libraries.spec.js

+7
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ describe('/libraries', () => {
5858
}
5959
});
6060
});
61+
62+
// Test with a trailing slash
63+
it('responds to requests with a trailing slash', async () => {
64+
const res = await request(path + '/');
65+
expect(res).to.have.status(200);
66+
expect(res.body).to.deep.equal(response.body);
67+
});
6168
});
6269

6370
describe('Requesting human response (?output=human)', () => {

0 commit comments

Comments
 (0)