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

  1. Log the wrapped error (%v) and the first bytes of the body to see what was actually returned.
  2. Confirm the request went directly to https://dev.to/api/articles and is not intercepted by a proxy.
  3. Compare the payload against the Article struct fields; update the struct if the API shape changed.
  4. 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

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.