Graylog 7 API: POST /api/streams returns 400 "entity cannot be null"
Symptom
A stream-creation call that worked against Graylog 6 now fails:
$ curl -sS -u "$USER:$PASS" -H 'X-Requested-By: cli' -H 'Content-Type: application/json' \
-d '{"title":"nginx","description":"nginx access logs","remove_matches_from_default_stream":false}' \
https://graylog.example.com/api/streams
HTTP/1.1 400 Bad Request
{"type":"ApiError","message":"entity cannot be null"}
Nothing in the message points at the payload shape, so the natural assumption — a missing required field
such as index_set_id — sends you down the wrong path.
Cause
In Graylog 7 the endpoint takes a CreateEntityRequest: the stream itself moved one level down, under an
entity key. Send the same fields wrapped:
$ curl -sS -u "$USER:$PASS" -H 'X-Requested-By: cli' -H 'Content-Type: application/json' \
-d '{"entity":{"title":"nginx","description":"nginx access logs","index_set_id":"'"$INDEX_SET"'","remove_matches_from_default_stream":false}}' \
https://graylog.example.com/api/streams
{"stream_id":"66d1f0a2c4e1b90f3a7d2c11"}
If you are porting scripts, expect the same envelope on sibling creation endpoints and check each one against your version rather than assuming.
The second trap: the root admin cannot have an API token
The obvious way to script this is a token instead of a password. Against the root admin account it fails:
$ curl -sS -u admin:"$PASS" -H 'X-Requested-By: cli' -X POST \
https://graylog.example.com/api/users/admin/tokens/automation
{"type":"ApiError","message":"hexString has 24 characters"}
The root user is defined in the configuration (GRAYLOG_ROOT_PASSWORD_SHA2 in .env), not as a document
in the database, so there is no user id to attach a token to — hence the complaint about a 24-character
hex string, which is what a MongoDB ObjectId looks like.
Do not work around this by rewriting GRAYLOG_ROOT_PASSWORD_SHA2 to a known plaintext hash just to get
API access: you change the credentials your operators use for the UI, and you still cannot create a token.
Create a dedicated database user with the Admin role in the UI instead, then either use
user:password from your credential store or issue a token for that account:
$ curl -sS -u svc-automation:"$SVC_PASS" -H 'X-Requested-By: cli' -X POST \
https://graylog.example.com/api/users/svc-automation/tokens/automation
{"name":"automation","token":"…","last_access":"1970-01-01T00:00:00.000Z"}
Verify
$ curl -sS -u svc-automation:"$SVC_PASS" https://graylog.example.com/api/streams \
| jq -r '.streams[] | "\(.id) \(.title)"'
66d1f0a2c4e1b90f3a7d2c11 nginx
A stream that exists but never matches anything is the next thing to check: rules are created separately, and an empty rule set on a non-default stream means zero messages, not “everything”.