charmbracelet/glow · error

unable to read http response body: %w

Error message

unable to read http response body: %w

What it means

glow fetches GitLab READMEs via the v4 REST API: it builds https://<host>/api/v4/projects/<path>, calls http.Get, then streams the body with io.ReadAll. This error means the HTTP request itself succeeded but reading the response body failed; the underlying I/O error is wrapped with %w. It almost always indicates a connection that dropped or was cut after the response headers were received.

Source

Thrown at gitlab.go:37

	projectPath := url.QueryEscape(owner + "/" + repo)

	type readme struct {
		ReadmeURL string `json:"readme_url"`
	}

	apiURL := fmt.Sprintf("https://%s/api/v4/projects/%s", u.Hostname(), projectPath)

	//nolint:bodyclose
	// it is closed on the caller
	res, err := http.Get(apiURL) //nolint: gosec,noctx
	if err != nil {
		return nil, fmt.Errorf("unable to get url: %w", err)
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read http response body: %w", err)
	}

	var result readme
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("unable to parse json: %w", err)
	}

	readmeRawURL := strings.ReplaceAll(result.ReadmeURL, "blob", "raw")

	if res.StatusCode == http.StatusOK {
		//nolint:bodyclose
		// it is closed on the caller
		resp, err := http.Get(readmeRawURL) //nolint: gosec,noctx
		if err != nil {
			return nil, fmt.Errorf("unable to get url: %w", err)
		}

		if resp.StatusCode == http.StatusOK {

View on GitHub (pinned to e3970c813d)

Solutions

  1. Retry the command - transient connection drops are the most common cause
  2. Reproduce outside glow: curl https://gitlab.com/api/v4/projects/<owner>%2F<repo> and watch for truncation
  3. For self-hosted GitLab, raise reverse-proxy read timeouts (e.g. nginx proxy_read_timeout)
  4. Test from a different network to rule out VPN/proxy interference
Defensive patterns

Strategy: retry

Validate before calling

func gitlabAPIReadable(host, projectPath string) error {
	u := fmt.Sprintf("https://%s/api/v4/projects/%s", host, projectPath)
	client := &http.Client{Timeout: 15 * time.Second}
	res, err := client.Get(u) //nolint:noctx
	if err != nil { return err }
	defer res.Body.Close()
	_, err = io.Copy(io.Discard, io.LimitReader(res.Body, 1<<20))
	return err
}

Type guard

func isTransientNetErr(err error) bool {
	var dnsErr *net.DNSError
	if errors.As(err, &dnsErr) { return dnsErr.IsTemporary }
	var opErr *net.OpError
	return errors.As(err, &opErr)
}

Try / catch

var body []byte
for attempt := 0; attempt < 3; attempt++ {
	var err error
	body, err = fetchGitLabAPI(u)
	if err == nil { break }
	if !isTransientNetErr(err) { return err }
	time.Sleep(time.Duration(attempt+1) * 500 * time.Millisecond)
}

Prevention

When it happens

Trigger: http.Get to the GitLab projects API returns headers, then the TCP connection resets or times out mid-body; a proxy or middlebox truncates the transfer; a TLS problem surfaces during body streaming; the server closes the connection early.

Common situations: Flaky Wi-Fi or VPN links, corporate proxies that cut long streams, self-hosted GitLab behind nginx/traefik with an aggressive proxy_read_timeout, CI runners with restricted egress.

Related errors


AI-assisted analysis of charmbracelet/glow@e3970c813d (2026-08-15). Data as JSON: /api/errors/bab31f9cd85db678. Report an issue: GitHub.