Serving precompressed brotli "octet-stream" files fail on Firefox

1. The problem I’m having:

I’m attempting to serve some precompressed brotli files using Caddy web server using the file_serverprecompressed br gzip directives. This works in most cases, but fails when the file is of the octet-stream type and the browser is Firefox. This works perfectly fine in Chrome. It also works fine in both browsers using Nginx. What is going on?

Edit: Everything also works fine when serving gzip files from Caddy to Firefox. It’s just the combination of brotli + octet-stream + firefox that has this weird behavior.

2. Error messages and/or full log output:

I enabled logging, but all I see are INFO logs that suggest everything within Caddy is working fine.

3. Caddy version:

caddy:2.6-alpine

4. How I installed and ran Caddy:

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /source/

RUN dotnet tool install --global dotnet-references
ENV PATH="${PATH}:/root/.dotnet/tools"

COPY *.sln ./
COPY */*.csproj ./

RUN dotnet-references fix --entry-point ./Crypter.sln --working-directory ./ --remove-unreferenced-project-files
RUN dotnet restore Crypter.Web

COPY ./ ./
RUN dotnet publish Crypter.Web --no-restore --configuration release --output /app/

FROM caddy:2.6-alpine AS webhost
COPY Crypter.Web/Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/wwwroot/ /srv/
EXPOSE 80
EXPOSE 443

a. System environment:

Docker running on Windows 10

b. Command:

docker-compose --profile dev up

c. Service/unit/compose file:

version: "3.9"
services:
  api:
    profiles:
      - web
      - dev
    image: ghcr.io/crypter-file-transfer/crypter_api:main
    build:
      context: .
      dockerfile: Crypter.API.Dockerfile
    expose:
      - "80"
    environment:
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT-Production}
      ASPNETCORE_URLS: http://0.0.0.0:80
      ASPNETCORE_TransferStorageSettings__Location: /mnt/storage
      CUSTOMCONNSTR_DefaultConnection: host=${POSTGRES_HOST:-db};database=crypter;user id=crypter_user;pwd=${POSTGRES_USER_PASSWORD:-dev};
      CUSTOMCONNSTR_HangfireConnection: host=${POSTGRES_HANGFIRE_HOST:-db};database=crypter_hangfire;user id=crypter_hangfire_user;pwd=${POSTGRES_HANGFIRE_USER_PASSWORD:-dev};
    volumes:
      - ${API_STORAGE_PATH}:/mnt/storage
      - ${API_SETTINGS_FILE}:/app/appsettings.json
    restart: always
    depends_on:
      db:
        condition: service_healthy
  web:
    profiles:
      - web
      - dev
    image: ghcr.io/crypter-file-transfer/crypter_web:main
    build:
      context: .
      dockerfile: Crypter.Web.Dockerfile
    ports:
      - ${WEB_BIND_PORT-80}:80
      - ${WEB_SECURE_BIND_PORT-443}:443
    environment:
      CRYPTER_API_BASE: http://api:80
      CADDY_HOST: ${CADDY_HOST}
      CADDY_OPTIONS: ${CADDY_OPTIONS}
      CADDY_TLS_VOLUME: ${CADDY_TLS_VOLUME}
    volumes:
      - ./Containers/Caddy/data:/data
      - ${CADDY_TLS_VOLUME}:/mnt/tls
    restart: always
  db:
    profiles:
      - db
      - dev
    image: postgres:15.2
    expose:
      - "5432"
    ports:
      - ${POSTGRES_BIND_IP-[::1]}:${POSTGRES_BIND_PORT-5432}:5432
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD-dev}
      POSTGRES_C_PASSWORD: ${POSTGRES_USER_PASSWORD:-dev}
      POSTGRES_HF_PASSWORD: ${POSTGRES_HANGFIRE_USER_PASSWORD:-dev}
    volumes:
      - ./Containers/PostgreSQL/data:/var/lib/postgresql/data
      - ./Containers/PostgreSQL/postgres-init-files:/docker-entrypoint-initdb.d
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d crypter -U postgres"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 60s

d. My complete Caddy config:

{$CADDY_HOST} {
    {$CADDY_OPTIONS}

    log {
        output file /var/log/caddy
        format console
        level INFO
    }

    route {
        reverse_proxy /api/* {$CRYPTER_API_BASE}

        try_files {path} /index.html
        root * /srv/

        file_server {
            precompressed br gzip
        }
    }
}

5. Links to relevant resources:

An observation: The files that fail to transfer do not have a content-type defined in the response header.

How can I set a “default” mime-type? That is, if no other appropriate mime-type is found, then use “my/default”.

Resolved: header ?Content-Type "application/octet-stream"

1 Like

:thinking: I think ordering the directives like this is a mistake? This will set root after having run try_files so try_files will have the wrong webroot set.

But instead of route, I’d write your config like this:

{$CADDY_HOST} {
	{$CADDY_OPTIONS}

	log {
		output file /var/log/caddy
		format console
	}

	handle /api/* {
		reverse_proxy {$CRYPTER_API_BASE}
	}

	handle {
		root * /srv
		try_files {path} /index.html
		file_server {
			precompressed br gzip
		}
	}
}
2 Likes

Thank you for the advice. I’m still new to Caddy and am not an expert with web servers in general.

I still need the header directive for Firefox to behave properly.

1 Like

If you copy the request Firefox made as cURL (use devtools for this), then run that curl command (add -v), what is the output? Paste both the command and the output here.

Jack@LAPTOP-Q6MG9K39 MINGW64 ~
$ curl -v --insecure -H "Accept-Encoding: br" https://localhost/_framework/dotnet.timezones.blat >> c:\\Users\\Jack\\Desktop\\output.txt
*   Trying 127.0.0.1:443...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: C:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt
*  CApath: none
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [15 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [925 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [80 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [36 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [36 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: [NONE]
*  start date: May 21 01:41:20 2023 GMT
*  expire date: May 21 13:41:20 2023 GMT
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x28063e72070)
} [5 bytes data]
> GET /_framework/dotnet.timezones.blat HTTP/2
> Host: localhost
> user-agent: curl/7.78.0
> accept: */*
> accept-encoding: br
>
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [130 bytes data]
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
} [5 bytes data]
< HTTP/2 200
< accept-ranges: bytes
< alt-svc: h3=":443"; ma=2592000
< content-encoding: br
< etag: "ruzjflwg3"
< last-modified: Fri, 24 Mar 2023 17:13:38 GMT
< server: Caddy
< vary: Accept-Encoding
< date: Sun, 21 May 2023 01:46:56 GMT
<
{ [5 bytes data]
100 42051    0 42051    0     0  2858k      0 --:--:-- --:--:-- --:--:-- 3158k
* Connection #0 to host localhost left intact

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