REST API create empty content when "Content-Type" header is not defined
Describe the Bug
Creating a user via curl and REST API with a few fields defined creates an empty user.
To Reproduce
Create the user via curl:
$ curl -X POST -H 'Authorization: Bearer admin-token' "http://127.0.0.1:8055/users" -d '{ "email": "[email protected]", "first_name":"hello" }'
{"data":{"id":"3c8d7d56-ba17-49cb-a09e-2adf64c42587","first_name":null,"last_name":null,"email":null,"password":null,"location":null,"title":null,"description":null,"tags":null,"avatar":null,"language":null,"tfa_secret":null,"status":"active","role":null,"token":null,"last_access":null,"last_page":null,"provider":"default","external_identifier":null,"auth_data":null,"email_notifications":true,"appearance":null,"theme_dark":null,"theme_light":null,"theme_light_overrides":null,"theme_dark_overrides":null,"policies":[]}}
The token used belongs to an Admin user
-> the user is created, but both email and first_name fields are ignored during creation (the response includes "first_name":null,"email":null,, which is incorrect.)
-> using Directus app/admin access, we can see the user has been created, but all fields are empty too (this is coherent with the POST JSON response)
Note: I'm running a docker image derived from Directus v11.0.2, containing curl binary. All curl calls are done from inside this custom docker container (no nginx or other RP in between).
If I perform the same curl call again, I have exactly the same behavior, and another empty user is created.
Same behavior again with an "empty json body": curl -X POST -H 'Authorization: Bearer admin-token' "http://127.0.0.1:8055/users" -d '{}'.
After further investigations, I found the cause: my curl cause does not have Content-Type: application/json defined. Using curl -v, I can see curl defaults to Content-Type: application/x-www-form-urlencoded, which I guess Directus does not understand, then it reads the request body as "empty string" no matter what I send.
As soon as I add -H 'Content-Type: application/json' curl's option, the user is created with the given fields correctly set:
$ curl -H 'Content-Type: application/json' -X POST -H 'Authorization: Bearer admin-token' "http://127.0.0.1:8055/users" -d '{ "email": "[email protected]", "first_name":"hello" }'
{"data":{"id":"e0bf25b6-f6b9-4619-a4eb-4ecf8592f993","first_name":"hello","last_name":null,"email":"[email protected]","password":null,"location":null,"title":null,"description":null,"tags":null,"avatar":null,"language":null,"tfa_secret":null,"status":"active","role":null,"token":null,"last_access":null,"last_page":null,"provider":"default","external_identifier":null,"auth_data":null,"email_notifications":true,"appearance":null,"theme_dark":null,"theme_light":null,"theme_light_overrides":null,"theme_dark_overrides":null,"policies":[]}}
If I retry, I now get an expected error:
$ curl -H 'Content-Type: application/json' -X POST -H 'Authorization: Bearer admin-token' "http://127.0.0.1:8055/users" -d '{ "email": "[email protected]", "first_name":"hello" }'
{"errors":[{"message":"Value for field \"email\" in collection \"directus_users\" has to be unique.","extensions":{"collection":"directus_users","field":"email","code":"RECORD_NOT_UNIQUE"}}]}
Still, I can call multiple times with empty JSON body:
$ curl -H 'Content-Type: application/json' -X POST -H 'Authorization: Bearer admin-token' "http://127.0.0.1:8055/users" -d '{}'
{"data":{"id":"4a190eca-aab5-4581-989e-43cd8b971ef5","first_name":null,"last_name":null,"email":null,"password":null,"location":null,"title":null,"description":null,"tags":null,"avatar":null,"language":null,"tfa_secret":null,"status":"active","role":null,"token":null,"last_access":null,"last_page":null,"provider":"default","external_identifier":null,"auth_data":null,"email_notifications":true,"appearance":null,"theme_dark":null,"theme_light":null,"theme_light_overrides":null,"theme_dark_overrides":null,"policies":[]}}
$ curl -H 'Content-Type: application/json' -X POST -H 'Authorization: Bearer admin-token' "http://127.0.0.1:8055/users" -d '{}'
{"data":{"id":"5637b9a7-9a55-4a6e-af7d-964122788e82","first_name":null,"last_name":null,"email":null,"password":null,"location":null,"title":null,"description":null,"tags":null,"avatar":null,"language":null,"tfa_secret":null,"status":"active","role":null,"token":null,"last_access":null,"last_page":null,"provider":"default","external_identifier":null,"auth_data":null,"email_notifications":true,"appearance":null,"theme_dark":null,"theme_light":null,"theme_light_overrides":null,"theme_dark_overrides":null,"policies":[]}}/
To recap, in my comprehension, there are 2 issues:
- Directus REST API should reject requests that does not have a valid Content-Type header (valid = a value Directus can handle correctly). Because according to the docs,
The API uses JSON for input and output, andapplication/x-www-form-urlencodedis not JSON.
I also tried with
'Content-Type: application/whatever'and with noContent-Typeheader at all, in both case an empty user is created
- empty body should not be allowed in the POST payload. Even if all users fields are optionnals, it should detect empty payload and rejects with an error like "no payload found".
I didn't test other REST API endpoints, but my guess is the behavior would be the same for endpoints where all fields are optionals (some POST, probably all PATCH). This may be highly mistaken because:
- you can create empty items in some collections
- you may receive a 200 OK after a PATCH, you think the update has been done, but no update has been performed. So one would have to check the HTTP Code 200 + the JSON response to ensure all required update has been done
Last but not least: This also may explain the behavior reported in issue https://github.com/directus/directus/issues/22826
Directus Version
v11.0.2
Hosting Strategy
Self-Hosted (Docker Image)
Database
sqlite
I don't know if I fully agree with statement 2 that empty post bodies should be prevented generally speaking, but 1 is a good suggestion!
This is more of a Documentation issue than a bug.
The following prevent us from doing so:
- Extensions can define their own content type or none at all so we cannot simply reject requests missing content-types
- Extensions can add their own content type parser
- Our import endpoint (e.g.
/import/:collection) accepts an optional multipart content type - Empty body for POST can be a valid request
Due to the above this is not as trivial as enforcing a content type or rejecting empty body POST requests
restricting the content type to always be enforced
This is not what I'm saying or suggesting :)
As you pointed out, the "correct" Content-Type highly depend on each endpoint you're talking to. For most Directus REST API endpoints, application/json seems the standard. Using another CT or none for these endpoint should be considered an error.
Some endpoint, like this "import endpoint", could accept multiple CT values and handle each accordingly.
For the extensions, the use might declare what Content-Type(s) its endpoint support, all other CT value being rejected.
So yes, I guess this is not trivial as this can not be a "one change to fix them all", but rather require a low-level change for each endpoint.