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
96 changes: 96 additions & 0 deletions .github/workflows/gateway-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Gateway Conformance

on:
workflow_dispatch:
push:
branches:
- main
- master
pull_request:
paths-ignore:
- '**/*.md'

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

steps:
# 1. Download the gateway-conformance fixtures
- name: Download gateway-conformance fixtures
uses: ipfs/gateway-conformance/.github/actions/extract-fixtures@v0.8
with:
output: fixtures
merged: true

# 2. Build frisbii
- name: Checkout frisbii
uses: actions/checkout@v5
with:
path: frisbii

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: frisbii/go.mod
cache-dependency-path: frisbii/go.sum

- name: Build frisbii
run: go build -o frisbii ./cmd/frisbii
working-directory: frisbii

# 3. Start frisbii with fixtures
- name: Start frisbii
run: |
./frisbii --listen=127.0.0.1:3747 --car=../fixtures/fixtures.car > frisbii.log 2>&1 &
FRISBII_PID=$!

# Wait for frisbii to be ready (any HTTP response including 404 is OK)
for i in {1..30}; do
if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3747 2>/dev/null | grep -q '^[0-9]'; then
echo "frisbii is ready (PID: $FRISBII_PID)"
exit 0
fi
echo "Waiting for frisbii to start (attempt $i/30)..."
sleep 1
done

echo "frisbii failed to start within 30 seconds"
echo "==> frisbii log:"
cat frisbii.log
exit 1
working-directory: frisbii

# 4. Run the gateway-conformance tests
- name: Run gateway-conformance tests
uses: ipfs/gateway-conformance/.github/actions/test@v0.8
with:
gateway-url: http://127.0.0.1:3747
specs: trustless-gateway
args: -skip 'TestGatewayIPNSRecord'
json: output.json
xml: output.xml
html: output.html
markdown: output.md

# 5. Upload the results
- name: Upload MD summary
if: failure() || success()
run: cat output.md >> $GITHUB_STEP_SUMMARY

- name: Upload HTML report
if: failure() || success()
uses: actions/upload-artifact@v4
with:
name: gateway-conformance.html
path: output.html

- name: Upload JSON report
if: failure() || success()
uses: actions/upload-artifact@v4
with:
name: gateway-conformance.json
path: output.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**An experimental minimal IPLD data provider for the IPFS network**

**Frisbii** is currently a simple HTTP server that conforms minimally to the [IPFS Trustless Gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/) specification, serving IPLD data in CAR format to HTTP clients.
**Frisbii** is currently a simple HTTP server that conforms minimally to the [IPFS Trustless Gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/) specification, serving IPLD data to HTTP clients. Only [`application/vnd.ipld.car`](https://www.iana.org/assignments/media-types/application/vnd.ipld.car) and [`application/vnd.ipld.raw`](https://www.iana.org/assignments/media-types/application/vnd.ipld.raw) content types are supported. IPNS resolution is not supported.

Content routing, when enabled, is delegated to the [Interpletary Network Indexer (IPNI)](https://cid.contact/) service. On startup, Frisbii can announce its content to IPNI, allowing it to be discovered by clients that query the indexer for that content.

Expand Down
18 changes: 16 additions & 2 deletions httpipfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,21 @@ func NewHttpIpfsHandlerFunc(
}
accept := accepts[0]

// Parse filename parameter - trustlesshttp.ParseFilename only accepts .car
// but frisbii also supports raw responses with .bin, so we handle that case
fileName, err := trustlesshttp.ParseFilename(req)
if err != nil {
logError(http.StatusBadRequest, err)
return
// If ParseFilename failed, check if it's because of .bin extension for raw response
if accept.IsRaw() && req.URL.Query().Get("filename") != "" {
fileName = req.URL.Query().Get("filename")
if !strings.HasSuffix(fileName, ".bin") {
logError(http.StatusBadRequest, fmt.Errorf("invalid filename parameter for raw response; expected .bin extension"))
return
}
} else {
logError(http.StatusBadRequest, err)
return
}
}

// validate CID path parameter
Expand Down Expand Up @@ -321,6 +332,9 @@ func NewHttpIpfsHandlerFunc(

close(bytesWrittenCh) // signal that we've started writing, so we can't log errors to the response now

if accept.IsCar() {
res.Header().Set("Accept-Ranges", "none")
}
res.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", fileName))
res.Header().Set("Cache-Control", trustlesshttp.ResponseCacheControlHeader)
res.Header().Set("Content-Type", accept.WithQuality(1).String())
Expand Down