benbjohnson/litestream · error
webdav: cannot write file %q: %w
Error message
webdav: cannot write file %q: %w
What it means
The final upload step calls client.WriteStreamWithLength to PUT the temp file to the WebDAV server with an explicit Content-Length. This error wraps any HTTP-level upload failure: rejected credentials, quota exceeded, connection drop, or server-side error responses.
Source
Thrown at webdav/replica_client.go:286
size, err := io.Copy(tmpFile, fullReader)
if err != nil {
return nil, fmt.Errorf("webdav: cannot copy to temp file: %w", err)
}
if _, err := tmpFile.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("webdav: cannot seek temp file: %w", err)
}
if err := client.MkdirAll(path.Dir(filename), 0755); err != nil {
return nil, fmt.Errorf("webdav: cannot create parent directory %q: %w", path.Dir(filename), err)
}
// Upload with Content-Length header using seekable temp file.
// WriteStreamWithLength requires both a seekable reader and known size,
// which we now have from the temp file. This avoids chunked encoding
// and ensures reliable uploads across all WebDAV server configurations.
if err := client.WriteStreamWithLength(filename, tmpFile, size, 0644); err != nil {
return nil, fmt.Errorf("webdav: cannot write file %q: %w", filename, err)
}
internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "PUT").Inc()
internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "PUT").Add(float64(size))
return <x.FileInfo{
Level: level,
MinTXID: minTXID,
MaxTXID: maxTXID,
Size: size,
CreatedAt: timestamp,
}, nil
}
func (c *ReplicaClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (_ io.ReadCloser, err error) {
client, err := c.init(ctx)
if err != nil {
return nil, errView on GitHub (pinned to 4ed7a308f6)
Solutions
- Check credentials and quota on the WebDAV server account
- Raise proxy body-size limits (e.g. nginx client_max_body_size) and timeouts
- Inspect the wrapped underlying gowebdav error for the exact HTTP status
- Test the upload path manually with curl -T file to isolate network vs auth issues
Defensive patterns
Strategy: retry
Validate before calling
// precheck: credentials and quota
client, _ := gowebdav.NewClient(serverURL, user, pass)
if err := client.Connect(); err != nil { return err }
probe, _ := os.CreateTemp("", "probe"); probe.WriteString("x")
err := client.Write(path.Join(basePath, ".probe"), probe)
probe.Close(); os.Remove(probe.Name())
if err != nil { return err } Try / catch
if err := writeLTX(ctx); err != nil {
if errors.Is(err, context.DeadlineExceeded) || isConnReset(err) { retry with exponential backoff }
// 401/403/507 are not retryable: fix credentials/quota first
} Prevention
- Rotate WebDAV credentials before expiry
- Monitor quota usage on the WebDAV account
- Raise proxy client_max_body_size and timeouts for large LTX files
- Use stable network paths for long uploads
When it happens
Trigger: PUT request to the WebDAV server fails: 401/403 auth failure, 507 insufficient storage quota, connection reset/timeout mid-upload, or server rejecting large bodies.
Common situations: Expired or wrong WebDAV credentials in config, storage quota full on the WebDAV account, reverse proxies (Nginx) with client_max_body_size limits, unstable connections dropping long uploads.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- webdav: cannot connect to server: %w
- webdav: cannot delete path %q: %w
- webdav: cannot read directory %q: %w
- heartbeat URL must be a valid HTTP or HTTPS URL
- failed to read response: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/62778a87ebad7455.
Report an issue: GitHub.