Show HN: Nano-web – a low latency one binary webserver designed for serving SPAs (github.com)
It uses a single compiled binary, it's going to have extremely low latency on account of caching all files in memory at runtime, and also the fun feature and really useful that's good for SPAs (e.g. Vite) or things like Astro is that you can inject configuration variables into it at runtime and access them from within your frontend code, so you don't have to rebuild any images for different environments as part of your CI.
Whilst I'm sure this problem has been solved time and time again, I could never get a solution to be quite right.
Also, serving things like Astro from S3 gets to be tricky because CloudFront doesn't support index pages in subdirectories so the routing breaks. This fixes this.
Use it or don't, I think it's a cool little project and it's been a while since I worked on and released something :)
118 comments
[ 4.5 ms ] story [ 178 ms ] threadHave you seen redbean, which is a universal binary and serves static files from a zipfile? How does this compare to that?
Honestly, I think that the current Web can easily use libp2p from IPFS and serve files from any browser peer to peer, with relays on the internet.
I think the Web lacks two major things:
1) Address resolution only uses federated DNS and you need extensions to use, say, self-sovereign Web3 domains which can’t be stolen
2) There is NO WAY to load files by their content hash like onion links in Tor. If there was, we’d have a much more secure Web.
I'm not much into them these days but if I remember correctly in the 4-5 years prior, they just works! In-fact, CloudFront was easier to serve static web assets than serving directly from S3.
Or have I misunderstood what you meant?
IE the it would happily resolve / to /index.html would be fine, but if you wanted to do /my-dir and have it resolve to /my-dir/index.html it would 404.
Did some quick searches and I think there seems to be some kinks indeed and of course the solutions. Here are a few;
- https://stackoverflow.com/questions/59634922/how-do-i-serve-...
- https://aws.amazon.com/blogs/networking-and-content-delivery...
Update/Edit: I think the setting to use correct S3 Endpoint makes a difference (which was indeed always my default way when I did).
https://stackoverflow.com/questions/31017105/how-do-you-set-...
Here's the request edge lambda:
https://github.com/radiosilence/cdk-static-web/blob/master/s...
Here's the response edge lambda:
https://github.com/radiosilence/cdk-static-web/blob/master/s...
It's been a while since I've updated that project though.
With nano-web for my current site (https://blit.cc) I am just spinning up a free-tier unikernel instance on EC2 and using it as the CF origin. Possibly even more cost effective than S3, and far simpler.
Response times for the "waiting" portion are usually around 5-10ms with CF and if I am going direct to the origin it's around 2-5ms
The problem is that essentially every time a user goes to the site it will be a 404 status code since they are probably not typing in example.com/index but this has pragmatically not been an issue for a wholly authenticated, private, B2B SaaS app. The marketing website is a separate subdomain.
For a public site this is probably worse than returning 200s that should be 404s occasionally, though.
[0]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/Custom...
I wrote something similar long ago in < 400 lines of Go (with most of it being templates and mimetype information) that just supports GET, HEAD and gzip, in case you're interested[4].
[1] https://caddyserver.com/docs/quick-starts/static-files
[2] https://www.npmjs.com/package/sirv-cli
[3] https://openwrt.org/docs/guide-user/services/webserver/http....
[4] https://github.com/supriyo-biswas/gohttpd
gzip is unnecessary because 99% of the traffic I would serve is going to be pre-compressed (like zip and jpeg files).
serve also comes to mind. If you have node installed, `npx serve .` does exactly that.
There are a few go projects that fit your description, none of them very popular, probably because they end up being a 100-line wrapper around http frameworks.
[1] https://caddyserver.com/
Minimality is not about size, it's about reducing the attack surface.
> There are a few go projects that fit your description, none of them very popular, probably because they end up being a 100-line wrapper around http frameworks.
Indeed. Everyone can write their own but dependencies will make its attack surface enormous and performance will probably be mediocre at best.
[0] https://filed.rkeene.org/
[0] https://rkeene.org/viewer/devel/vmi586/main.c.htm
[1] https://redbean.dev
Redbean in general seems great and really interesting.
readme: http://canonical.org/~kragen/sw/dev3/httpdito-readme
source (single file): http://canonical.org/~kragen/sw/dev3/server.s
it might be reasonable to configure its port and mime types map on the command line instead of in the source, but at the moment it's arguably a bit too zero-configuration
it runs on linux, can handle about 20000 hits per second, has about 20 kilobytes of memory footprint when idle, and needs another 4 kilobytes per concurrently open connection (though the kernel needs several times that)
20000 hits per second is not really fast in the sense that other servers on the same hardware and kernel can handle more than twice that, but it can saturate a gigabit pipe if your files average 7 kilobytes or more
It's written in Go, has zero third party dependencies, supports optional basic auth, and optional TLS cert generation (self signed) for local HTTPS.
Not updated in a while though.
And if you drop the requirement for minimalism I'd second the recommendation for Caddy instead.
https://github.com/dclowd9901/posse
Here's[5] an example I put together that I have running on fly.io[6]. I'm doing a redirect for root path, but it could easily be changed to serve up the contents of index.html.
I use a similar deployment for a small site I created[7] to generate UUIDs. Kemal[8] also looks like a good alternative.
----
Update:
Here's[9] an example also running on fly.io[10] using Kemal[8]
[1] https://crystal-lang.org/api/1.11.2/HTTP/Server.html
[2] https://crystal-lang.org/reference/1.11/guides/static_linkin...
[3] https://docs.docker.com/build/building/multi-stage/
[4] https://hub.docker.com/_/scratch/
[5] https://github.com/adrianlv512/minimal-server
[6] https://minimal-server.fly.dev
[7] https://uuid-generator.org/
[8] https://kemalcr.com/
[9] https://github.com/adrianlv512/minimal-server-kemal
[10] https://minimal-server-kemal.fly.dev
https://github.com/radiosilence/nano-web?tab=readme-ov-file#...
https://github.com/lipanski/docker-static-website
https://github.com/PierreZ/goStatic
Be careful: SPAs can handle 404s client-side.
All you need to do is continue to return the 404 result code and include the same exact content that you normally do.
Then your SPA recognizes that the route isn't set up and displays the error page.
That said, thinking about it - the server has no idea whether a client side route is actually correct.
You could have https://example.com/potato/tomato which will not exist on the server of course, but will be handled by client side JS on /index.html and could be a correct route within the app, so it is safer to 200 it.
SPAs aren't really generally made for SEO also. With something like Astro which is SEO friendly, it could actually return a 200 as expected and a 404 if not.
How is the search engine going to find a page that doesn't exist, isn't in a sitemap, isn't linked to in any nav anchors, in order to index it?
If links are never going to be used, there is no point in doing the effort of client side routing.
If links are going to used, the server needs to respond with the correct status or you break the assumptions upon which the tools people use to access the web are based.
Return a non-200 response doesn't mean you can't return a page. You can indeed display the current sale in the content returned with a 404 response and most sites do. A SPA absolutely can return tue full index.html content for every end point, but it shouldn't lie with it's status codes in the process.
> The SPA server is under no obligation to serve any page with any specific response code so long as it is valid HTTP.
You are also under no obligation to close your html tags or accessibility guidelines. However, ignoring specs like this can break things for your users in ways you can't always predict, especially for more marginal use cases.
If you're gonna to write a SPA, please, I beg to you, actually take the time to do it properly and release tools that enable that. Otherwise you are directly contributing to making the internet worse.
I don't think it really matters as much as you say it matters. It isn't "making the internet worse", it's only affecting the specific site's indexing, so don't act like this happening is bleeding out to other sites and affecting everyone. Calm down, "the internet" isn't getting worse from this practice.
>A SPA absolutely can return tue full index.html content for every end point, but it shouldn't lie with it's status codes in the process.
That's your opinion. I'm fine with it returning a 200. The thing about SPAs is "the server" doesn't always know if a route is valid or invalid. Hosting an SPA on S3 is one example of that. It responds to requests made to unfound resources with the index page, and a 200 status. It does not know if /account or /config does or does not exist in the front-end routing, the server is dumb and can only serve assets that it knows exist. So it returns the content for the index page and the front-end then decides if the route is valid or not, because the front end state changes with the URL, and there could be thousands or billions of different routes, especially if data is included in the route. There is no way to set up the server to know every route that might happen when it encounters something like /account/verify/8ab2c09f43e7b2a094839ab3cef329
In this case the back end serves index.html with status 200 and the front-end sorts out what to do.
And I'm fine with it, this doesn't "make the internet worse", only the SPA site is affected (if it really has any effect at all).
You can serve pages on your site however you want to, I don't really care what your site does.
There is even a sample template repos. https://medium.com/disney-streaming/api-driven-spa-with-ngin...
Looking forward to diving into nano-web
https://github.com/varbhat/serv
* Serve the SPA (fallback to index.html): `serv -spa`
* Serve the directory and all it's contents: : `serv -dir=/path/to/my/dir`
https://github.com/radiosilence/nano-web?tab=readme-ov-file#...
Currently working on automating releasing the unikernel OPS package (https://repo.ops.city/v2/packages/radiosilence/show) but I had to submit a patch to the OPS project that needs to be released first (or I can make a custom image to use with GHA to build it with a version from master).
Either way, I figure the more projects that support it natively, the bigger the ecosystem grows and the more data we'll see based on real world usage.
The tooling seems pretty good, though the documentation could do with some improvement (I intend to do more discovery and PR some documentation improvements when I have time).
I like your approach towards this, I'm thinking the same. I appreciate the response you provided, I've been planning on playing around with it for a while when I get the time. Your comment made me even more interested.
It went quite quick from seeing your comment on the post about Nanos to your own project integrating it!
It also precaches gzipped and brotli'd versions of sensible files so that it can accept-encoding for a variety of browsers.
I see it as a pragmatic trade off between speed and RAM. Even free tier EC2 instance should have enough RAM to handle pretty much any SPA even if the text content is duplicated (compressed) especially seeing as with a unikernel you have no RAM used up by Linux processes.
If you're really concerned you can MMAP the file instead, and do a read every once in a while to keep it cached.
There are a lot of cases where this kind of functionality could cause big problems, and very few where it has more than a marginal improvement over what your OS is already doing. Just read the file every once in a while to keep it in memory cache.
(It's negated by the SSL handshake, but that's another discussion...)
I ask because those numbers seem really off.
Some things we might have 1000+ checkboxs to work through on a checklist (cut down by some initial tailoring steps). Even things with a small wait on a click etc are frustrating. If you can drive using a keyboard shortcut and get very immediate response you can really move through things.
Latency Numbers Everyone Should Know: https://static.googleusercontent.com/media/sre.google/en//st...
sendfile(2): https://man7.org/linux/man-pages/man2/sendfile.2.html
Given that point, I got the impression that the comparison was against loading resources from S3. The difference between that and serving from memory (even between that and serving from uncached traditional hard-drive) is going to be significant.
EDIT: cursory research suggests S3 latency (https://stackoverflow.com/a/861539/114292, being the most recent reference that turned up) is smaller than it was last time I remember needing to look into it, but still of the order of 10+ms.
Plus, like you said, 10-20ms extra on top of the actual latency from the server is not going to make any difference to the end user.
There are use cases where you probably need something like Nginx or whatever to handle lots of requests without a CDN but these are rare.
Reading assets from files instead of a RAM cache won’t add “10-20ms”. This is measured in microseconds, not milliseconds. Most servers nowadays are using SSDs or eNVM. And as I wrote earlier, the files are likely already cached in RAM by the OS anyway :)
I know. I was just using a hypothetical worst case scenario to exemplify that it doesn't matter.
oof, as someone who frequently has to run DAST tools, returning a 200 for something that should be a 404 is the bane of my existence.
The tool will try to request sensitive files by trying something as simple as fetching hxxps://example.com/../../../etc/passwd. If you return a "200 OK", it will flag as a security issue, even if the resulting data isn't a passwd file.
I don't think you can convince me that returning a 200 OK and some default content for a URL that doesn't exist is ever the correct thing to do. If you want to use 301 or 302 to redirect them to the front page, that's probably fine, but the most correct thing to do is 404.
How is the webserver supposed to know in advance which URLs the SPA is able to handle?
And then how are you handling routes with params inside? In your SPA they would probably be configured like
Now you would have to translate that to something the webserver config understands, probably wildcard / glob pattern / regex stuff and splice that into the web server config?I mean it's possible, sure, but that's a lot of overhead and room for error just to deploy a simple SPA, it definitely is an opinionated approach.
The way I've implemented SPAs is to run nginx. All my JS and CSS is in /static/ and served up by nginx. "/" returns an index.html. "/api/*" gets forwarded to a back end of some sort, usually Python's Bottle because it's lightweight and simple to use. If the route doesn't exist, Bottle returns a 404. If I have an endpoint that includes an ID of some sort in the URL, like /api/getobject/12345, but you request an object that doesn't exist, then I still return a 404.
How are you implementing SPAs in a way that you can't return a 404?
Not specifically me, but a lot of SPAs have their own routing. So you would go to
When you hit that route, the webserver returns /index.html with the JS bundle, the JS runs and matches the user ID and post ID from the URL and then maybe makes some API requests to fetch content of whatever, I mean the SPA doesn't have to be backed by an API anyways, could be purely static but still use a router.In the React world, react-router is (was) pretty popular:
https://github.com/remix-run/react-router
That's a library that works exactly that way, relying on every route being passed to the JS bundle for handling, so the webserver needs to be configured to never return 404 and just return the index.html for every route, here is an example of what this looks like from the docs of Caddy, another popular webserver with a pretty easy to understand config format:
https://caddyserver.com/docs/caddyfile/patterns#single-page-...
It's a "Single Page App", you may have many pages being rendered by this app, but it's a single html page that is responsible for doing this.
Now – SPAs are not as popular as they used to be, and there is an argument to be made that they aren't the best solution in most cases with what's available now, regardless of that they do exist and this appears to be a web server specifically for them so having the option handle SPA routing as it's commonly done, does make sense.
And I think for stuff like internal management apps, admin panels etc. SPAs with their own routing are still a decent solution.
What's available now that superceds them?
For mostly static or content driven sites, there are really nice static site generators now. I find Astro[1] really pleasant to use, offering the convenience of SPA framework tooling, while allowing me to build sites that run without the need for any client-side JavaScript but the option to into client-side JS as needed.
But also NextJS has a similar static export function, and of course there are many other great SSGs outside the JavaScript world like Hugo.
For dynamic web applications like the example from my comment I would just do server side rendering using your language and framework of choice, Phoenix, Rails, Next, Django, whatever.
In both cases the issue with 404 routes is avoided.
[1] https://astro.build/
The mostly same stuff as before, but slightly better. SPAs were a fad that got very popular, but they were generally a poor solution in most cases and as the fad faded, they stopped being as overused.
One of the reasons why SPAs are bad solution is because they interfere with how the web works in significant ways (browser history, caching, server response statuses) and solving those issues without degrading user experience is hard.
When a SPA was actually useful, it was worth spending the time to solve those issues.
The overuse of SPAs means that most of them did not take the time to solve these issues properly so most SPAs sucked (and still do). This is why I don't understand releasing a SPA server in 2024 that doesn't even try to provide tools to solve those issues.
So like, if you're on my-spa.com, and click a link to a post, the JS loads the post and updates the URL, but doesn't perform a full page load. So while you might be on https://my-spa.com/user/39820002/post/39811155, your browser likely never actually sent a GET request to /user/39820002/post/39811155. But this creates the issue that if you manually navigate to that URL, your browser WILL send a request for it.
In that case, yeah, I think the correct thing to do is to return the 200 OK with your usual index.html. When the JS makes the API requests, those requests should return a 404, and the JS will then render a page that says "That post doesn't exist" or something.
But in the case of a request that couldn't possibly exist, like a GET request for /etc/passwd, they should still be 404s. Any router should be able to do that.
EDIT: I think a better way to do this hold thing is to make the resource an anchor tag in the URL. Instead of https://my-spa.com/user/39820002/post/39811155, make it https://my-spa.com/#user/39820002/post/39811155
Exactly, you got it. This is why SPA configurations for webservers exist, like the SPA_MODE on the one we are commenting on.
> EDIT: I think a better way to do this hold thing is to make the resource an anchor tag in the URL. Instead of https://my-spa.com/user/39820002/post/39811155, make it https://my-spa.com/#user/39820002/post/39811155
As someone else said:
https://news.ycombinator.com/item?id=39819120
Hash routes have their own problems, I don't think they are a great solution, but indeed SPA routers often support them, example from react router:
https://reactrouter.com/en/main/router-components/hash-route...
Note that they strongly suggest to not use hash routing unless you have no other choice.
I agree that returning 200 to every route isn't great for some scenarios, but realistically, SPAs exist, they usually work like this and they are deployed like this, can't really change that now. I would just not build an SPA with client side routing at this point in time since there are solutions that I personally prefer, other people may think differently though.
Also it is nice to be able to cleanly implement SPAs in such a way that the server itself is doing as little work as possible for cost and scale.
The problem is that when you only serve your app from `/`, and then the user navigates via the app to `/user/123`, the url gets updated, but if the user tries to reload the page, they'll end up on a 404 page because you're not serving the app from `/user/123`. There's a few ways to solve this, each again with tradeoffs, but one way is to have the server return 200 and the contents of the main page any time that the user requests an unknown route, under the assumption that the user is probably trying to navigate to a dynamic route handled by the frontend.
> I don't think you can convince me that [...] is ever the correct thing to do
It's just a matter of trade-offs.
Serving the app.js on a 404 can cause things like browser/caching issues that are confusing for the user, and 404 isn't necessarily correct since the app is both servable and recoverable from that url.
And I'm not sure 302-redirecting is any less meaningless than serving 200 on a not-found. The user doesn't need the redirect since the app is already capable of handling the not-found page in a user-friendly way. So does it somehow improve things for programmatic analysis of the app to make all 40X errors redirect to the homepage, and is that very compelling?
Is there a benefit for doing so?
If your main routing logic is in your JS app your server can't know if a route exists or not unless you duplicate routing code across front end and backend
The complaint is serving a 200 for invalid routes, which breaks all kinds of things and violates web standards.
Heh, not a bad criticism. Though TBH, I think all DAST tools are hare-brained. They get sold as automated penetration tests, but they really can't figure out SPAs at all, since they often only look at the immediate response to an HTTP request, when often, you'd need to see exactly what the JS code does with the response and how it gets rendered in the page. Maybe a PUT/POST request to upload data returns a blank 200 OK, but only a follow-up GET request will actually show the result.
They end up with more false negatives than false positives. At best, they'll catch the low-hanging fruits involving missing headers that often lead to Beg Bounties like Clickjacking or missing HSTS.
The only reason DAST still exists is because compliance frameworks still list it as a checkbox. Otherwise, I think they're pretty useless.
Is HTTP/2 or HTTP/3 even useful behind a service mesh (istio/envoy etc.)?
* BusyBox httpd [1]
* thttpd [2]
* Lwan Web Server [3]
* gatling [4]
I built and maintain a super tiny Docker image (~158KB) for serving static files [5]. My current implementation is based on Busybox httpd and scratch but I also tried out thttpd in the past, which worked well but came with a high memory footprint in some cases. thttpd was used to serve big projects back in the day but it doesn't seem maintained any more. The other two projects were on my list of things to try out. All these projects should compile to binaries <500KB.
By comparison nano-web builds a 18.5MB image on my machine. In my experience you can't go below 5-6MB with Alpine (scratch is the way to go if you can compile statically) and you can't go below ~500KB with Golang or Rust, at least not for serving http requests (C or ASM is probably the way to go).
Note that I'm not saying that small is always better, it probably depends on your use case and there are reasons for using nginx for a static website as well.
[1] https://www.busybox.net/ [2] https://www.acme.com/software/thttpd/ [3] https://lwan.ws/ [4] https://www.fefe.de/gatling/ [5] https://github.com/lipanski/docker-static-website
Would buying a virtual server and running any one of these mini servers to run your site make it more performant than throwing it on a regular host like Dreamhost?
Like, does the simplicity in the server itself offer ANY speed/performance/latency advantages for a site that is ultimately showcasing big imagery for a folio?
> you can inject configuration variables into it at runtime and access them from within your frontend code
Is there a way to do that with BusyBox httpd or the others?