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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ The following emojis are used to highlight certain changes:

### Added

- `gateway`: Added retrieval state tracking for timeout diagnostics. When retrieval timeouts occur, the error messages now include detailed information about which phase failed (path resolution, provider discovery, connecting, or data retrieval) and provider statistics. [#1015](https://github.com/ipfs/boxo/pull/1015)
- `gateway`: Enhanced error handling and UX for timeouts:
- Added retrieval state tracking for timeout diagnostics. When retrieval timeouts occur, the error messages now include detailed information about which phase failed (path resolution, provider discovery, connecting, or data retrieval) and provider statistics including failed peer IDs [#1015](https://github.com/ipfs/boxo/pull/1015) [#1023](https://github.com/ipfs/boxo/pull/1023)
- Added `Config.DiagnosticServiceURL` to configure a CID retrievability diagnostic service. When set, 504 Gateway Timeout errors show a "Check CID retrievability" button linking to the service with `?cid=<failed-cid>` [#1023](https://github.com/ipfs/boxo/pull/1023)
- Improved 504 error pages with "Retry" button, diagnostic service integration, and clear indication when timeout occurs on sub-resource vs root CID [#1023](https://github.com/ipfs/boxo/pull/1023)

### Changed

Expand Down
9 changes: 6 additions & 3 deletions gateway/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ type DagTemplateData struct {

type ErrorTemplateData struct {
GlobalData
StatusCode int
StatusText string
Error string
StatusCode int
StatusText string
Error string
DiagnosticServiceURL string
RootCID string
FailedCID string
}

type DirectoryTemplateData struct {
Expand Down
36 changes: 28 additions & 8 deletions gateway/assets/error.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,44 @@
{{ else if eq .StatusCode 500 }}
<p>This gateway was unable to return the requested data due to an internal error. Please check the error below for more information.</p>
{{ else if eq .StatusCode 502 }}
<p>The gateway backed was unable to fullfil your request due to an error.</p>
<p>The gateway backend was unable to fulfill your request due to an error.</p>
{{ else if eq .StatusCode 504 }}
<p>The gateway backend was unable to fullfil your request due to a timeout.</p>
<p>This gateway is taking too long to fetch content from content providers. Please check the error below for more information.</p>

{{ if and .RootCID .FailedCID (ne .RootCID .FailedCID) }}
<p><strong>Note:</strong> The timeout occurred while fetching a sub-resource (CID: <code>{{ .FailedCID }}</code>) of the root content (CID: <code>{{ .RootCID }}</code>).</p>
{{ end }}

{{ else if eq .StatusCode 506 }}
<p>The gateway backend was unable to fullfil your request at this time. Try again later.</p>
<p>The gateway backend was unable to fulfill your request at this time. Try again later.</p>
{{ end }}

<pre class="terminal wrap">{{ .Error }}</pre>

<p>How you can proceed:</p>
<ul>
<li>Check the <a href="https://discuss.ipfs.tech/c/help/13" rel="noopener noreferrer">Discussion Forums</a> for similar errors.</li>
<li>Try diagnosing your request with the <a href="https://docs.ipfs.tech/reference/diagnostic-tools/" rel="noopener noreferrer">diagnostic tools</a>.</li>
<li>Self-host and run an <a href="https://docs.ipfs.tech/concepts/ipfs-implementations/" rel="noopener noreferrer">IPFS client</a> that verifies your data.</li>
<li>Check the <a href="https://discuss.ipfs.tech/c/help/13" target="_blank" rel="noopener noreferrer">Discussion Forums</a> for similar errors.</li>
<li>Try diagnosing your request with the <a href="https://docs.ipfs.tech/reference/diagnostic-tools/" target="_blank" rel="noopener noreferrer">diagnostic tools</a>.</li>
<li>Self-host and run an <a href="https://docs.ipfs.tech/concepts/ipfs-implementations/" target="_blank" rel="noopener noreferrer">IPFS client</a> that verifies your data.</li>
{{ if or (eq .StatusCode 400) (eq .StatusCode 404) }}
<li>Inspect the <a href="https://cid.ipfs.tech/" rel="noopener noreferrer">CID</a> or <a href="https://explore.ipld.io/" rel="noopener noreferrer">DAG</a>.</li>
<li>Inspect the <a href="https://cid.ipfs.tech/" target="_blank" rel="noopener noreferrer">CID</a> or <a href="https://explore.ipld.io/" target="_blank" rel="noopener noreferrer">DAG</a>.</li>
{{ end }}
</ul>

<div class="buttons">
<button onclick="document.querySelector('#main .container').innerHTML = '<p>Retrying, please wait...</p>'; window.location.reload(true)"
class="button retry-button">
Retry
</button>
{{ if and (eq .StatusCode 504) .FailedCID .DiagnosticServiceURL }}
<a href="{{ .DiagnosticServiceURL }}?cid={{ .FailedCID }}"
target="_blank"
rel="noopener noreferrer"
class="button diagnose-button">
Check CID retrievability
</a>
{{ end }}
</div>
</section>
</main>
</body>
Expand Down
45 changes: 39 additions & 6 deletions gateway/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ a:hover {
text-decoration: underline;
}

a:active,
a:visited {
color: #00b0e9;
}

.flex {
display: flex;
}
Expand Down Expand Up @@ -235,7 +230,7 @@ section > .grid.dag > div:nth-of-type(2n+1) {
.terminal {
background: var(--steel-gray);
color: white;
padding: .7em;
padding: 0.625em 1.25em;
border-radius: var(--radius);
word-wrap: break-word;
white-space: break-spaces;
Expand Down Expand Up @@ -273,3 +268,41 @@ section > .grid.dag > div:nth-of-type(2n+1) {
display: none;
}
}

.buttons {
margin: 1.5em 0;
display: flex;
gap: 0.75em;
flex-wrap: wrap;
}

.button {
display: inline-block;
padding: 0.625em 1.25em;
font-size: 1em;
cursor: pointer;
border-radius: var(--radius);
text-decoration: none;
border: none;
font-family: inherit;
}

.retry-button {
background-color: #3B8C90;
color: var(--near-white);
}

.retry-button:hover {
background-color: var(--navy);
}

.diagnose-button {
background-color: var(--steel-gray);
color: var(--near-white);
}

.diagnose-button:hover {
background-color: var(--navy);
color: var(--near-white);
text-decoration: none;
}
10 changes: 10 additions & 0 deletions gateway/backend_blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,8 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.Immu
// Update retrieval progress, if tracked in the existing context
if retrievalState := retrieval.StateFromContext(ctx); retrievalState != nil {
retrievalState.SetPhase(retrieval.PhasePathResolution)
// Set the root CID (first CID in the path)
retrievalState.SetRootCID(contentPath.RootCid())
}

/*
Expand Down Expand Up @@ -726,6 +728,14 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.Immu
}

pathRoots = pathRoots[:len(pathRoots)-1]

// Set the terminal CID after successful path resolution
if retrievalState := retrieval.StateFromContext(ctx); retrievalState != nil {
if rootCid := lastPath.RootCid(); rootCid.Defined() {
retrievalState.SetTerminalCID(rootCid)
}
}

return pathRoots, lastPath, remainder, nil
}

Expand Down
7 changes: 7 additions & 0 deletions gateway/backend_car.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ func resolvePathWithRootsAndBlock(ctx context.Context, p path.ImmutablePath, uni
// Update retrieval progress, if tracked in the existing context
if retrievalState := retrieval.StateFromContext(ctx); retrievalState != nil {
retrievalState.SetPhase(retrieval.PhasePathResolution)
// Set the root CID (first CID in the path)
retrievalState.SetRootCID(p.RootCid())
}
md, terminalBlk, err := resolvePathToLastWithRoots(ctx, p, unixFSLsys)
if err != nil {
Expand All @@ -234,6 +236,11 @@ func resolvePathWithRootsAndBlock(ctx context.Context, p path.ImmutablePath, uni
}
}

// Set the terminal CID after successful path resolution
if retrievalState := retrieval.StateFromContext(ctx); retrievalState != nil {
retrievalState.SetTerminalCID(terminalCid)
}

return md, terminalBlk, err
}

Expand Down
28 changes: 25 additions & 3 deletions gateway/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/ipfs/boxo/gateway/assets"
"github.com/ipfs/boxo/path"
"github.com/ipfs/boxo/path/resolver"
"github.com/ipfs/boxo/retrieval"
"github.com/ipfs/go-cid"
ipld "github.com/ipfs/go-ipld-format"
"github.com/ipld/go-ipld-prime/datamodel"
Expand Down Expand Up @@ -186,15 +187,36 @@ func writeErrorResponse(w http.ResponseWriter, r *http.Request, c *Config, statu
}

if acceptsHTML {
// Extract CIDs from RetrievalState for diagnostic purposes (504 errors)
var rootCID, failedCID string
if statusCode == http.StatusGatewayTimeout && c != nil && c.DiagnosticServiceURL != "" {
if retrievalState := retrieval.StateFromContext(r.Context()); retrievalState != nil {
// Get root CID (first CID in the path)
if root := retrievalState.GetRootCID(); root.Defined() {
rootCID = root.String()
}
// Get terminal CID (CID that failed to retrieve)
if terminal := retrievalState.GetTerminalCID(); terminal.Defined() {
failedCID = terminal.String()
} else {
// If no terminal CID, use root CID as the failed CID
failedCID = rootCID
}
}
}

w.Header().Set("Content-Type", "text/html")
w.WriteHeader(statusCode)
err := assets.ErrorTemplate.Execute(w, assets.ErrorTemplateData{
GlobalData: assets.GlobalData{
Menu: c.Menu,
},
StatusCode: statusCode,
StatusText: http.StatusText(statusCode),
Error: message,
StatusCode: statusCode,
StatusText: http.StatusText(statusCode),
Error: message,
DiagnosticServiceURL: c.DiagnosticServiceURL,
RootCID: rootCID,
FailedCID: failedCID,
})
if err != nil {
fmt.Fprintf(w, "error during body generation: %v", err)
Expand Down
27 changes: 27 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"io"
"net/url"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -32,6 +33,9 @@ const (
//
// See the MaxConcurrentRequests field documentation for detailed tuning guidance.
DefaultMaxConcurrentRequests = 4096

// DefaultDiagnosticServiceURL is the default URL for CID diagnostic service
DefaultDiagnosticServiceURL = "https://check.ipfs.network"
)

// Config is the configuration used when creating a new gateway handler.
Expand Down Expand Up @@ -115,11 +119,34 @@ type Config struct {
// A value of 0 disables the limit entirely (use with caution).
MaxConcurrentRequests int

// DiagnosticServiceURL is the URL for a service that can be used to diagnose
// issues with CID retrievability. When the gateway returns a 504 Gateway Timeout
// error, an "Inspect retrievability of CID" button will be shown that links to
// this service with the CID as a query parameter. When set to empty string, the
// button is not shown.
DiagnosticServiceURL string

// MetricsRegistry is the Prometheus registry to use for metrics.
// If nil, prometheus.DefaultRegisterer will be used.
MetricsRegistry prometheus.Registerer
}

// validateConfig validates and normalizes the Config, returning a modified copy.
// Invalid values are logged and set to safe defaults.
func validateConfig(c Config) Config {
// Validate DiagnosticServiceURL
if c.DiagnosticServiceURL != "" {
if _, err := url.Parse(c.DiagnosticServiceURL); err != nil {
log.Errorf("invalid DiagnosticServiceURL %q: %v, disabling diagnostic service", c.DiagnosticServiceURL, err)
c.DiagnosticServiceURL = ""
}
}

// Future validations can be added here

return c
}

// PublicGateway is the specification of an IPFS Public Gateway.
type PublicGateway struct {
// Paths is explicit list of path prefixes that should be handled by
Expand Down
3 changes: 3 additions & 0 deletions gateway/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ type handler struct {
//
// [IPFS HTTP Gateway]: https://specs.ipfs.tech/http-gateways/
func NewHandler(c Config, backend IPFSBackend) http.Handler {
// Validate and normalize configuration
c = validateConfig(c)

// Get registry from config or use default
reg := c.MetricsRegistry
if reg == nil {
Expand Down
Loading
Loading