Skip to content

Commit 7773857

Browse files
authored
Add post: Dropping requests in SvelteKit (#947)
Fixes #946
1 parent 23a8bf0 commit 7773857

9 files changed

Lines changed: 129 additions & 2 deletions

File tree

.github/copilot-instructions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ This is my personal website with posts and notes.
2929
- Do not use self-closing tags for Svelte components and HTML elements.
3030
- Use TailwindCSS for styling and tailwind-merge for class merging.
3131
- Comments use proper punctuation and end with a period.
32+
33+
## Writing style
34+
35+
- Use American English.
36+
- Be concise and clear in your explanations.
37+
- Use active voice and present tense.
38+
- Write a tad informal, but not too casual.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { resolvePost } from '$lib/server/resolvers';
2+
import postDroppingRequestsInSvelteKit from '$posts/(2025)/dropping-requests-in-sveltekit/meta';
3+
import { json } from '@sveltejs/kit';
4+
import type { RequestHandler } from './$types';
5+
6+
export const prerender = true;
7+
8+
export const GET: RequestHandler = async (event) => {
9+
// Sort order: latest first.
10+
const posts = [postDroppingRequestsInSvelteKit];
11+
12+
const transformedPosts = await Promise.all(
13+
posts.map((post) => {
14+
return resolvePost({ postMeta: post, event });
15+
})
16+
);
17+
18+
return json(transformedPosts);
19+
};

src/routes/api/posts/all/+server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';
12
import { json } from '@sveltejs/kit';
23
import type { RequestHandler } from './$types';
3-
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';
44

55
// Needs to be set explicitly because we prerender endpoint `/sitemap.xml`.
66
export const prerender = true;
@@ -25,5 +25,9 @@ export const GET: RequestHandler = async ({ fetch }) => {
2525
response = await fetch('/api/posts/2024');
2626
posts = [...((await response.json()) as ResolvedPost[]), ...posts];
2727

28+
// Fetch 2025 posts.
29+
response = await fetch('/api/posts/2025');
30+
posts = [...((await response.json()) as ResolvedPost[]), ...posts];
31+
2832
return json(posts);
2933
};

src/routes/api/posts/latest/+server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';
12
import { json } from '@sveltejs/kit';
23
import type { RequestHandler } from './$types';
3-
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';
44

55
// No need to set `export const prerender = true;`.
66
// Prerendering is triggered by `/`, which uses this endpoint and iself is prerendered.
@@ -17,5 +17,9 @@ export const GET: RequestHandler = async ({ fetch }) => {
1717
response = await fetch('/api/posts/2024');
1818
posts = [...((await response.json()) as ResolvedPost[]), ...posts];
1919

20+
// Fetch 2025 posts.
21+
response = await fetch('/api/posts/2025');
22+
posts = [...((await response.json()) as ResolvedPost[]), ...posts];
23+
2024
return json(posts.slice(0, 10));
2125
};

src/routes/api/tags/[id]/tags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const tags: Tag[] = [
4242
label: 'Gitpod',
4343
path: '/tags/gitpod'
4444
},
45+
{ id: 'railway', label: 'Railway', path: '/tags/railway' },
4546
{
4647
id: 'screen-recording',
4748
label: 'Screen recording',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { resolvePost } from '$lib/server/resolvers';
2+
import type { PageServerLoad } from './$types';
3+
import meta from './meta';
4+
5+
export const load: PageServerLoad = async (event) => {
6+
const post = await resolvePost({ postMeta: meta, event });
7+
const { title, description, ogImageUrl } = post;
8+
9+
return { post, seo: { title, description, ogImageUrl } };
10+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<script>
2+
import { Figure, ResponsiveImage } from '@maiertech/sveltekit-helpers';
3+
import srcProbingBots from './probing-bots.png';
4+
</script>
5+
6+
After migrating my website to [Railway](https://railway.com/), I noticed bots probing for
7+
accidentally exposed vulnerable files:
8+
9+
<Figure caption="HTTP Logs from Railway after first deployment with a custom domain." class="mb-8">
10+
<ResponsiveImage src={srcProbingBots} alt="Log entries on Railway.com showing bot requests to
11+
potentially exposed files, for example, `/.env`." intrinsicWidth={1032} aspectRatio={16/9}></ResponsiveImage>
12+
</Figure>
13+
14+
Since my website is built with SvelteKit, it returns a 404 for these types of requests. Nothing to
15+
worry about in terms of security. However, all these 404 responses are processed by SvelteKit and
16+
consume resources on the server. This is especially annoying because Railway's pricing model is
17+
based on the resources a deployment consumes.
18+
19+
The obvious solution is to host the SvelteKit app behind a web application firewall (WAF) that
20+
blocks such requests before they reach the server. Unfortunately, Railway does not currently offer a
21+
WAF. So, I thought, why not let SvelteKit play WAF and make it drop these requests?
22+
23+
Here is what I came up with:
24+
25+
<Figure caption="hooks.server.ts" class="mb-8">
26+
27+
```ts
28+
import { type Handle } from '@sveltejs/kit';
29+
import { Blocklist } from '$lib/utils/index.js';
30+
import { BLOCKED_PATHS } from '$lib/blocklists/index.js';
31+
32+
const pathBlocklist = new Blocklist(BLOCKED_PATHS);
33+
34+
export const handle: Handle = async ({ event, resolve }) => {
35+
const { url } = event;
36+
37+
if (pathBlocklist.isBlocked(url.pathname)) {
38+
return new Response(null, { status: 204 });
39+
}
40+
41+
return resolve(event);
42+
};
43+
```
44+
45+
</Figure>
46+
47+
Inside the `handle` hook in `hooks.server.ts`, I check if the request path is on a blocklist. The
48+
blocklist is a
49+
[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) that
50+
contains paths used by bots from my Railway logs. Since SvelteKit handles requests at the
51+
application layer, it always wants to return a response. Even if I return `undefined` after
52+
detecting a malicious request, SvelteKit still returns a 500 server error.
53+
54+
A 500 server error probably consumes the same amount of resources as the original 404 response. So,
55+
I don't gain anything with this approach. The 204 no content response in the code above might shave
56+
off a little bit of processing compared to a 404 or 500 status. But it still returns a response,
57+
which also messes up my Railway logs because 204 responses show up as successful requests.
58+
59+
Unfortunately, SvelteKit cannot drop requests at the application layer. The only option is to send a
60+
response as early as possible to avoid wasting server resources.
61+
62+
So, what did I do instead? I proxied the SvelteKit app through
63+
[Cloudflare](https://www.cloudflare.com/). Its firewall and bot detection take care of malicious
64+
requests and make sure they never reach the SvelteKit app hosted on Railway. Not exactly an elegant
65+
solution, but it works.
66+
67+
Cloudflare and [Vercel](https://vercel.com/) have invested a lot into their WAFs lately, and if you
68+
have ever checked your WAF logs, you might have been stunned by how much garbage they block. I hope
69+
Railway (and other boutique hosters) will also offer a basic WAF in the not-too-distant future.
70+
After all, I want the non-big-tech hosting competition to succeed and be a viable option.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { PostMeta } from '@maiertech/sveltekit-helpers';
2+
3+
export default {
4+
title: 'Dropping requests in SvelteKit',
5+
author: 'thilo',
6+
publishedDate: '2025-07-27',
7+
description:
8+
"SvelteKit can't truly drop bad requests, so I use Cloudflare's WAF to block bots before they reach my Railway-hosted app.",
9+
tags: ['svelte', 'railway'],
10+
path: '/posts/dropping-requests-in-sveltekit',
11+
filepath: 'src/routes/posts/(2025)/dropping-requests-in-sveltekit/+page.svx'
12+
} satisfies PostMeta;
166 KB
Loading

0 commit comments

Comments
 (0)