Serving SPA with exception for certain paths

1. Caddy version (caddy version):

2.5.1

2. How I run Caddy:

Docker containers on AWS behind a load balancer.

a. System environment:

Docker containers on AWS.

b. Command:

na

c. Service/unit/compose file:

na

d. My complete Caddyfile or JSON config:

{
	admin off
	auto_https off
}
:80
file_server
root * /srv
encode gzip

@blob_path {
	path_regexp ^/[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$
}
handle @blob_path

handle {
	route {
		try_files {path} /
	}
}

handle /api/* {
	header {
		Access-Control-Allow-Origin "*"
		Access-Control-Allow-Methods "POST, GET, OPTIONS"
		Access-Control-Allow-Headers "Content-Type"
	}
	reverse_proxy http://api:80
}

3. The problem I’m having:

Not a problem per se.

The above config is working.

I just don’t understand exactly why it’s working, even after reading the docs. I arrived at this solution after reading the docs, trying to understand them as best I could, and some trial and error.

Background:

Our Caddy serves a static Angular SPA at index.html which calls an api route (another container), and delivers up a randomly-generated blob: file via javascript. The file has a guid-like name, for example:

blob:https://example.com/10c40926-3202-4060-ae97-fe9769d7fa16

The blob file is generated randomly, so it should be handled separately from the SPA (e.g., not routed to index.html), which I why I created a named matcher @blob_path.

My main question is why I need a separate handler and route for the root page at / in addition to the other handle directives.

handle {
	route {
		try_files {path} /
	}
}

4. Error messages and/or full log output:

na

5. What I already tried:

My initial understanding was that handle blocks make them mutually-exclusive from each other.

Based on this assumption, with the other 2 handlers in place I tried using no handler for /, simply:

try_files {path} /

This worked for the SPA, but did not work for the blob: files since they seemed to be routed to /, even with the other @blob_path handler.

I also tried giving / it’s own handle, with no route, in an attempt to give it some exclusivity:

handle {
	try_files {path} /
}

This caused the site to not load at all.

Neither of these two approaches allow the site to be served as a normal SPA, and allow the blob: file to bypass index.html.

Maybe I am not understanding something fundamental about the handler chain/order but any insight would be most welcome.

6. Links to relevant resources:

https://caddyserver.com/docs/caddyfile/directives/route Serving SPA with exception for certain paths

Putting try_files in its own handle should do very little other than ensuring it doesn’t run until handle does, essentially delaying its logic a bit. handles are also only exclusive from other same-level handles.

The fact the behavior suits your requirements with this setup is interesting, though, with the handleroute as well as:

Are you just leaving this handle empty?

Also, these blobs - are they just being left in your web root (i.e. /srv)? Are they the only other files that might exist being requested, e.g. is that regex strictly necessary or can you simply rely on file existence, like it seems you’re doing already?

Honestly there’s a lot of complexity there that I’m not sure is necessary. The way I’m reading it, you have a fairly simple decision tree:

  • Is it an API request? Proxy to the API.
  • If not? Try to serve a file:
    • Does the file exist on disk? Serve it.
    • Does it not exist? Route to index file.

If so, I’d have assumed a much simpler setup would work for you:

{
  admin off
  auto_https off
}

http:// {
  handle /api/* {
    reverse_proxy http://api:80
    header {
      Access-Control-Allow-Origin "*"
      Access-Control-Allow-Methods "POST, GET, OPTIONS"
      Access-Control-Allow-Headers "Content-Type"
    }
  }

  handle {
    root * /srv
    try_files {path} /
    file_server
    encode gzip
  }
}
3 Likes

Thanks for your extensive and well-thought-out reply!

The blobs are generated by an in-browser request to the api url, and the data is streamed back to the browser and constructed into application/pdf content-type with in-browser javascript. They are never stored on disk, which is why I thought I needed that extra named matcher to just do nothing, as you noted.

I am going to experiment with your simplified setup over the next week or so and report back.

2 Likes

Ahh, so - does that mean the request is actually made to /api/10c40926-3202-4060-ae97-fe9769d7fa16 or similar?

Is a request ever made to Caddy directly in the form /10c40926-3202-4060-ae97-fe9769d7fa16? It sounds like it might not be.

If the files aren’t stored on disk, then Caddy won’t be able to select for them with try_files, making that directive effectively just a rewrite /.

2 Likes

Yep, you’re right. I watched the logs very closely today and this is indeed the case. No need for any special handlers at all. The devs threw me for a loop on this one, that’s for sure.

In the end your simplified setup worked perfectly, so I marked it as the solution. Thanks again.

3 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.