wtfutil/wtf · error
decoding response: %w
Error message
decoding response: %w
What it means
After a 200 OK, FetchArticles decodes the response body into []Article with json.NewDecoder(...).Decode. If the body is not a JSON array matching the Article struct (or is empty/truncated), the decode error is wrapped as "decoding response: %w". This typically means the API answered 200 but did not return the expected article JSON.
Source
Thrown at modules/devto/client.go:79
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %d from DEV.to API", resp.StatusCode)
}
var articles []Article
if err := json.NewDecoder(resp.Body).Decode(&articles); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return articles, nil
}
View on GitHub (pinned to bb838c1ccb)
Solutions
- Log the wrapped error (%v) and the first bytes of the body to see what was actually returned.
- Confirm the request went directly to https://dev.to/api/articles and is not intercepted by a proxy.
- Compare the payload against the Article struct fields; update the struct if the API shape changed.
- Retry on transient truncation; treat persistent mismatch as an API contract change.
Example fix
// before
if err := json.NewDecoder(resp.Body).Decode(&articles); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
// after
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
if err := json.Unmarshal(body, &articles); err != nil {
return nil, fmt.Errorf("decoding response %q: %w", truncate(body, 200), err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Peek content-type before decoding
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") {
return fmt.Errorf("expected JSON, got Content-Type %q", ct)
} Type guard
func asDecodeError(err error) (*json.SyntaxError, bool) {
var se *json.SyntaxError
if errors.As(err, &se) {
return se, true
}
return nil, false
} Try / catch
articles, err := client.FetchArticles(ctx)
if err != nil {
var se *json.SyntaxError
if errors.As(err, &se) {
log.Printf("non-JSON body from DEV.to at offset %d: %v", se.Offset, err)
return
}
log.Printf("devto fetch failed: %v", err)
} Prevention
- Check Content-Type of upstream responses before decoding.
- Capture a snippet of failed bodies for diagnosis (limit read size).
- Pin/update the Article struct when the DEV.to API schema changes.
When it happens
Trigger: DEV.to returns 200 with an HTML page (proxy/captive portal), an empty body, or a JSON shape that fails unmarshal into []Article (e.g., an object instead of an array, or incompatible field types).
Common situations: Corporate proxies intercepting TLS, DEV.to serving a maintenance page with 200, API version change altering the articles payload, or truncated responses on flaky connections.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
AI-assisted analysis of wtfutil/wtf@bb838c1ccb (2026-09-03).
Data as JSON: /api/errors/d281d01a2bf2961e.
Report an issue: GitHub.