Missing Content-Type when cached /ui response in production

To get a good security score we are using the response header X-Content-Type-Options "nosniff", however that breaks when using the /ui in production.

The problem seems to be that the cached response from src/ServiceStack/TemplatePagesFeature.cs doesn’t preserve the Content-Type.

To reproduce:

mkdir Example
cd Example
x new empty
ASPNETCORE_ENVIRONMENT=Production dotnet run --project Example --configuration=Release --no-launch-profile

Then in another window run: curl -s --compressed -i https://localhost:5001/ui/Hello | head -n 9 and observe the difference between the first and second response:

# curl -s --compressed -i https://localhost:5001/ui/Hello | head -n 9
HTTP/1.1 200 OK
Content-Type: text/html
Date: Fri, 10 Jun 2022 23:09:59 GMT
Server: Kestrel
Content-Encoding: deflate
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" style="">

# curl -s --compressed -i https://localhost:5001/ui/Hello | head -n 9
HTTP/1.1 200 OK
Date: Fri, 10 Jun 2022 23:10:01 GMT
Server: Kestrel
Cache-Control: public, max-age=3600, must-revalidate
Content-Encoding: deflate
ETag: "9fa685d2d9a7e74d9b0cfc38b4a24728"
Transfer-Encoding: chunked

<!DOCTYPE html>

The first response correctly has the Content-Type set, and works correctly with the nosniff header. It is however missing the ETag and Cache-Control headers. I think caching should be enabled from the very first request, not just the second.

The second response has the ETag and Cache-Control headers, but is missing the Content-Type.

I would have expected the headers of the two responses to be the same.

This should be resolved with the latest v6.1.1 on MyGet.

Alternatively you can resolve it with previous versions by disabling compression:

ConfigurePlugin<UiFeature>(feature => {
    feature.Module.EnableCompression = false;
});

Thank you for the swift response.
Disabling compression does work, but is less good for performance.

Unfortunately you fix doesn’t seem to solve the problem.

To reproduce:

mkdir Example
cd Example
x new empty
x mix myget
dotnet restore
dotnet list package
ASPNETCORE_ENVIRONMENT=Production dotnet run --project Example --configuration=Release --no-launch-profile

The dotnet list package returns:

dotnet list package            
Project 'Example' has the following package references
   [net6.0]: 
   Top-level Package      Requested   Resolved
   > ServiceStack         6.1.1       6.1.1   

When testing the response from another terminal I get:

Example curl -s --compressed -i https://localhost:5001/ui/Hello | head -n 9
HTTP/1.1 200 OK
Content-Length: 47193
Content-Type: text/html; charset=utf-8
Date: Mon, 13 Jun 2022 12:07:45 GMT
Server: Kestrel
Content-Encoding: deflate

<!DOCTYPE html>
<html lang="en" style="">
➜  Example curl -s --compressed -i https://localhost:5001/ui/Hello | head -n 9
HTTP/1.1 200 OK
Content-Length: 47193
Date: Mon, 13 Jun 2022 12:07:47 GMT
Server: Kestrel
Cache-Control: public, max-age=3600, must-revalidate
Content-Encoding: deflate
ETag: "d45dc8f5170510be762b8fcd8cabab90"

<!DOCTYPE html>

So as before, the first response is missing Cache-Control and ETag. You didn’t comment on why they are not present in the first response.

The second response is missing the Content-Type. Seeing your fix adds the httpRes.ContentType I can only assume it is null and therefore has no effect.

I do notice that a problem with nosniff has appeared before, and they could quite possibly have experienced the same problem.

Maybe I should have opened this as a support ticket on GitHub instead?

Can retry with the latest v6.1.1 on MyGet, you’ll need to clear your NuGet cache:

$ nuget locals all -clear

With your next fix it works correctly!

➜  Example curl -s --compressed --http2 -i https://localhost:5001/ui/Hello | head -n 10
HTTP/1.1 200 OK
Content-Length: 47536
Content-Type: text/html; charset=utf-8
Date: Tue, 14 Jun 2022 20:21:14 GMT
Server: Kestrel
Cache-Control: public, max-age=3600, must-revalidate
Content-Encoding: deflate
ETag: "00eed2331e4124e342aa2cdd6949f532"

<!DOCTYPE html>
➜  Example curl -s --compressed --http2 -i https://localhost:5001/ui/Hello | head -n 10
HTTP/1.1 200 OK
Content-Length: 47536
Content-Type: text/html; charset=utf-8
Date: Tue, 14 Jun 2022 20:21:15 GMT
Server: Kestrel
Cache-Control: public, max-age=3600, must-revalidate
Content-Encoding: deflate
ETag: "00eed2331e4124e342aa2cdd6949f532"

<!DOCTYPE html>
➜  Example 

Thank you for the quick response and fix!

1 Like