siyuan-note/siyuan · error

read custom emoji response failed: %w

Error message

read custom emoji response failed: %w

What it means

Wraps an io.ReadAll failure while reading the (LimitReader-capped, maxCustomEmojiSize+1 bytes) response body of a downloaded emoji URL. The %w preserves the underlying network/IO cause (connection reset, TLS handshake drop, read deadline, EOF mid-chunk).

Source

Thrown at kernel/api/system.go:348

	if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" {
		return nil, fmt.Errorf("invalid custom emoji URL")
	}

	response, err := util.NewCustomReqClient().R().Get(parsedURL.String())
	if err != nil {
		return nil, fmt.Errorf("download custom emoji failed: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("download custom emoji failed with status %d", response.StatusCode)
	}
	if response.ContentLength > maxCustomEmojiSize {
		return nil, fmt.Errorf("custom emoji file is too large")
	}

	data, err := io.ReadAll(io.LimitReader(response.Body, maxCustomEmojiSize+1))
	if err != nil {
		return nil, fmt.Errorf("read custom emoji response failed: %w", err)
	}
	return data, nil
}

func normalizeCustomEmojiData(data []byte) (normalized []byte, ext string, err error) {
	if len(data) == 0 {
		return nil, "", fmt.Errorf("custom emoji file must not be empty")
	}

	raster := true
	switch http.DetectContentType(data) {
	case "image/png":
		ext = ".png"
	case "image/jpeg":
		ext = ".jpg"
	case "image/gif":
		ext = ".gif"
	case "image/webp":

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Retry the request once or twice; transient mid-stream failures often succeed on retry thanks to NewCustomReqClient's retry semantics.
  2. Download the file locally with curl/wget to confirm the source is stable; if it also fails locally, the host is the problem.
  3. If the host is unreliable, download manually and upload via the file form field instead of url.
  4. Inspect the wrapped error (errors.Unwrap) to distinguish TLS errors from plain EOF.

Example fix

// before
data, err := downloadCustomEmojiData(rawURL)
if err != nil { /* generic failure, no retry */ }

// after: surface the wrapped cause and retry transient IO errors
data, err := downloadCustomEmojiData(rawURL)
if err != nil {
    var netErr net.Error
    if errors.As(err, &netErr) && netErr.Timeout() {
        // retry once
        data, err = downloadCustomEmojiData(rawURL)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: confirm the URL responds quickly with a HEAD
resp, err := util.NewCustomReqClient().R().Head(rawURL)
if err != nil || resp.StatusCode != http.StatusOK {
    return fmt.Errorf("emoji source not reliably reachable")
}

Try / catch

var data []byte
var err error
for attempt := 0; attempt < 2; attempt++ {
    data, err = downloadCustomEmojiData(rawURL)
    if err == nil { break }
    var netErr net.Error
    if errors.As(err, &netErr) && (netErr.Timeout() || errors.Is(err, io.ErrUnexpectedEOF)) {
        continue // retry transient mid-stream IO failure
    }
    break // non-transient, stop
}

Prevention

When it happens

Trigger: POST /api/system/addCustomEmoji with url pointing at a host that starts a 200 response then drops the connection mid-transfer, or a flaky mobile network causing the read to abort.

Common situations: Unstable mobile/hotspot connection during download; source server closing keep-alive early; intermediary proxy truncating large responses; TLS renegotiation failure after headers.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/98f2019529b7ae5a. Report an issue: GitHub.