Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ This is my personal website with posts and notes.
- Do not use self-closing tags for Svelte components and HTML elements.
- Use TailwindCSS for styling and tailwind-merge for class merging.
- Comments use proper punctuation and end with a period.

## Writing style

- Use American English.
- Be concise and clear in your explanations.
- Use active voice and present tense.
- Write a tad informal, but not too casual.
19 changes: 19 additions & 0 deletions src/routes/api/posts/2025/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { resolvePost } from '$lib/server/resolvers';
import postDroppingRequestsInSvelteKit from '$posts/(2025)/dropping-requests-in-sveltekit/meta';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const prerender = true;

export const GET: RequestHandler = async (event) => {
// Sort order: latest first.
const posts = [postDroppingRequestsInSvelteKit];

const transformedPosts = await Promise.all(
posts.map((post) => {
return resolvePost({ postMeta: post, event });
})
);

return json(transformedPosts);
};
6 changes: 5 additions & 1 deletion src/routes/api/posts/all/+server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';

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

// Fetch 2025 posts.
response = await fetch('/api/posts/2025');
posts = [...((await response.json()) as ResolvedPost[]), ...posts];

return json(posts);
};
6 changes: 5 additions & 1 deletion src/routes/api/posts/latest/+server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import type { ResolvedPost } from '@maiertech/sveltekit-helpers';

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

// Fetch 2025 posts.
response = await fetch('/api/posts/2025');
posts = [...((await response.json()) as ResolvedPost[]), ...posts];

return json(posts.slice(0, 10));
};
1 change: 1 addition & 0 deletions src/routes/api/tags/[id]/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const tags: Tag[] = [
label: 'Gitpod',
path: '/tags/gitpod'
},
{ id: 'railway', label: 'Railway', path: '/tags/railway' },
{
id: 'screen-recording',
label: 'Screen recording',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { resolvePost } from '$lib/server/resolvers';
import type { PageServerLoad } from './$types';
import meta from './meta';

export const load: PageServerLoad = async (event) => {
const post = await resolvePost({ postMeta: meta, event });
const { title, description, ogImageUrl } = post;

return { post, seo: { title, description, ogImageUrl } };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script>
import { Figure, ResponsiveImage } from '@maiertech/sveltekit-helpers';
import srcProbingBots from './probing-bots.png';
</script>

After migrating my website to [Railway](https://railway.com/), I noticed bots probing for
accidentally exposed vulnerable files:

<Figure caption="HTTP Logs from Railway after first deployment with a custom domain." class="mb-8">
<ResponsiveImage src={srcProbingBots} alt="Log entries on Railway.com showing bot requests to
potentially exposed files, for example, `/.env`." intrinsicWidth={1032} aspectRatio={16/9}></ResponsiveImage>
</Figure>

Since my website is built with SvelteKit, it returns a 404 for these types of requests. Nothing to
worry about in terms of security. However, all these 404 responses are processed by SvelteKit and
consume resources on the server. This is especially annoying because Railway's pricing model is
based on the resources a deployment consumes.

The obvious solution is to host the SvelteKit app behind a web application firewall (WAF) that
blocks such requests before they reach the server. Unfortunately, Railway does not currently offer a
WAF. So, I thought, why not let SvelteKit play WAF and make it drop these requests?

Here is what I came up with:

<Figure caption="hooks.server.ts" class="mb-8">

```ts
import { type Handle } from '@sveltejs/kit';
import { Blocklist } from '$lib/utils/index.js';
import { BLOCKED_PATHS } from '$lib/blocklists/index.js';

const pathBlocklist = new Blocklist(BLOCKED_PATHS);

export const handle: Handle = async ({ event, resolve }) => {
const { url } = event;

if (pathBlocklist.isBlocked(url.pathname)) {
return new Response(null, { status: 204 });
}

return resolve(event);
};
```

</Figure>

Inside the `handle` hook in `hooks.server.ts`, I check if the request path is on a blocklist. The
blocklist is a
[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) that
contains paths used by bots from my Railway logs. Since SvelteKit handles requests at the
application layer, it always wants to return a response. Even if I return `undefined` after
detecting a malicious request, SvelteKit still returns a 500 server error.

A 500 server error probably consumes the same amount of resources as the original 404 response. So,
I don't gain anything with this approach. The 204 no content response in the code above might shave
off a little bit of processing compared to a 404 or 500 status. But it still returns a response,
which also messes up my Railway logs because 204 responses show up as successful requests.

Unfortunately, SvelteKit cannot drop requests at the application layer. The only option is to send a
response as early as possible to avoid wasting server resources.

So, what did I do instead? I proxied the SvelteKit app through
[Cloudflare](https://www.cloudflare.com/). Its firewall and bot detection take care of malicious
requests and make sure they never reach the SvelteKit app hosted on Railway. Not exactly an elegant
solution, but it works.

Cloudflare and [Vercel](https://vercel.com/) have invested a lot into their WAFs lately, and if you
have ever checked your WAF logs, you might have been stunned by how much garbage they block. I hope
Railway (and other boutique hosters) will also offer a basic WAF in the not-too-distant future.
After all, I want the non-big-tech hosting competition to succeed and be a viable option.
12 changes: 12 additions & 0 deletions src/routes/posts/(2025)/dropping-requests-in-sveltekit/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { PostMeta } from '@maiertech/sveltekit-helpers';

export default {
title: 'Dropping requests in SvelteKit',
author: 'thilo',
publishedDate: '2025-07-27',
description:
"SvelteKit can't truly drop bad requests, so I use Cloudflare's WAF to block bots before they reach my Railway-hosted app.",
tags: ['svelte', 'railway'],
path: '/posts/dropping-requests-in-sveltekit',
filepath: 'src/routes/posts/(2025)/dropping-requests-in-sveltekit/+page.svx'
} satisfies PostMeta;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.