siyuan-note/siyuan · error

the request body is empty or truncated (EOF)

Error message

the request body is empty or truncated (EOF)

What it means

Decode reads the HTTP request body into the contract's field map via json.Decoder.Decode. When the body is completely empty or ends prematurely, Decode returns io.EOF, which this code replaces with an explicit message before wrapping it with the failing contract path. It exists so callers can distinguish 'no/short body' from malformed JSON.

Solutions

  1. Ensure the client sends a valid JSON body with the required fields for this endpoint
  2. Check Content-Length / Transfer-Encoding and that the client actually flushes the payload
  3. If the endpoint legitimately accepts no body, mark its contract field with LegacyOptionalBody
  4. Inspect intermediaries (proxies, load balancers) that may truncate request bodies

Example fix

// before: client sends request without body
req, _ := http.NewRequest("POST", url, nil)
// after
body := []byte(`{"id":"20240101120000-abc"}`)
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
Defensive patterns

Strategy: validation

Validate before calling

function ensureBody(body) {
  if (!body || (typeof body === "string" && body.trim() === "")) throw new Error("Request body is empty; supply a JSON payload")
  const text = typeof body === "string" ? body : JSON.stringify(body)
  try { JSON.parse(text) } catch (e) { throw new Error("Request body is not complete JSON: " + e.message) }
  return text
}

Type guard

function hasBody(req) { return req.body !== undefined && req.body !== null && Object.keys(req.body).length > 0 }

Try / catch

try { await api.call(payload) } catch (e) { if (/empty or truncated \(EOF\)/.test(e.message)) { console.error("Body was empty/truncated — check client serialization and proxies", payload) } else { throw e } }

Prevention

When it happens

Trigger: Sending a POST/PUT request to an apicontract endpoint with a zero-length body, a Content-Length larger than the actual bytes sent, or a connection cut mid-body so the JSON is truncated.

Common situations: Forgetting to pass a body for endpoints whose contract does not declare LegacyOptionalBody; proxy/gateway buffering stripping the body; streaming clients that close the connection before flushing; tests calling the handler directly with no io.Reader content.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/306f9070bf96dfa0. Report an issue: GitHub.

Appendix: source

Thrown at kernel/apicontract/decode.go:38

	}
	if e.decodeRequest != nil {
		return e.decodeRequest(reader)
	}
	if e.definition.Body == StructJSONBody {
		err = json.NewDecoder(reader).Decode(&request)
		if err != nil {
			err = fmt.Errorf("Parses request [%s] failed: %s", e.definition.Path, err)
		}
		return
	}
	var fields map[string]json.RawMessage
	err = json.NewDecoder(reader).Decode(&fields)
	if err != nil {
		if e.definition.Body == LegacyOptionalBody {
			return request, nil
		}
		if errors.Is(err, io.EOF) {
			err = errors.New("the request body is empty or truncated (EOF)")
		}
		return request, fmt.Errorf("Parses request [%s] failed: %s", e.definition.Path, err)
	}
	value := reflect.ValueOf(&request).Elem()
	if value.Kind() != reflect.Struct {
		return request, fmt.Errorf("request contract must be a struct")
	}
	err = decodeRequestFields(value, fields)
	return request, err
}

func decodeRequestFields(value reflect.Value, fields map[string]json.RawMessage) error {
	for i := 0; i < value.NumField(); i++ {
		field := value.Type().Field(i)
		name := strings.Split(field.Tag.Get("json"), ",")[0]
		if (!field.IsExported() && !field.Anonymous) || name == "-" {
			continue
		}

View on GitHub (pinned to 9f775e8a12)