gohugoio/hugo · error

Error: Line did not parse to a valid JSON object\n

Error message

Error: Line did not parse to a valid JSON object\n

What it means

Printed by genwebp's parse_input_message (webp.c:339) when json_parse_string succeeded but the root JSON value is not a JSONObject. The worker frees the value and returns an empty InputMessage with no response. Same semantics as the genavif counterpart (821).

Source

Thrown at internal/warpc/genwebp/webp.c:339

    config->method = opts.method;

    return 1;
}

InputMessage parse_input_message(const char *line)
{
    InputMessage msg = {0};

    JSON_Value *root_value = json_parse_string(line);
    if (root_value == NULL)
    {
        fprintf(stderr, "Error parsing JSON line\n");
        return msg;
    }

    if (json_value_get_type(root_value) != JSONObject)
    {
        fprintf(stderr, "Error: Line did not parse to a valid JSON object\n");
        json_value_free(root_value);
        return msg;
    }

    JSON_Object *root_object = json_value_get_object(root_value);

    JSON_Object *header_object = json_object_get_object(root_object, "header");
    if (header_object != NULL)
    {
        msg.header.version = (int)json_object_get_number(header_object, "version");
        msg.header.id = (int)json_object_get_number(header_object, "id");
        const char *command_str = json_object_get_string(header_object, "command");
        if (command_str != NULL)
        {
            strncpy(msg.header.command, command_str, sizeof(msg.header.command) - 1);
            msg.header.command[sizeof(msg.header.command) - 1] = '\0';
        }
        const char *err_str = json_object_get_string(header_object, "err");

View on GitHub (pinned to 52c9bd7908)

Solutions

  1. Send only a single JSON object per line with a `header` containing `command` and `id`.
  2. Validate the root token type on the Go side with json.Decoder before writing.
  3. Diff the envelope against parse_input_message and write_output_message schemas.
  4. Add a regression test asserting a non-object root is handled deterministically.

Example fix

// before
"decode"

// after
{"header":{"version":1,"id":42,"command":"decode"},"data":{"params":{}}}
Defensive patterns

Strategy: validation

Validate before calling

func mustBeJSONObject(b []byte) error {
    dec := json.NewDecoder(bytes.NewReader(b))
    tok, err := dec.Token()
    if err != nil { return err }
    if d, ok := tok.(json.Delim); !ok || d != '{' {
        return fmt.Errorf("command root is not a JSON object")
    }
    return nil
}

Prevention

When it happens

Trigger: The host sends a JSON array, bare string/number, or any non-object root; or a framing bug concatenates objects so parson yields a non-object root.

Common situations: Hand-rolled test clients sending `[...]` instead of `{...}`, a protocol-version mismatch with a new batch format, leftover bytes from a prior command making the line start with a stray token, or a JSON producer emitting a top-level scalar under error.

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/0f820b05de2e34c0. Report an issue: GitHub.