Describe the bug
POST /auth/sign-in/ (email/password) returns an unhandled HTTP 500 when the
request does not send a User-Agent header. The failure happens after the
credentials are verified, so a client with correct credentials still cannot sign in.
Any programmatic client that omits a User-Agent hits this (for example a CLI/API
consumer; Rust's reqwest sends no User-Agent by default).
Root cause
AuthenticationAdapter.save_user_data() reads the header with no default:
# apps/api/plane/authentication/adapter/base.py
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
When the header is absent, .get("HTTP_USER_AGENT") returns None. The field is
declared:
# apps/api/plane/db/models/user.py
last_login_uagent = models.TextField(blank=True)
blank=True but not null=True, so the column is NOT NULL. Saving None raises an
IntegrityError. It is not an AuthenticationException, so it is not caught by the
sign-in view's handler and Django returns a generic 500.
The sibling code path already handles this correctly, which is the inconsistency:
# apps/api/plane/authentication/utils/login.py
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
To reproduce
Sign in with valid credentials but with the User-Agent header removed:
# 1) get a CSRF token and the csrftoken cookie
curl -s -c cookies.txt http://<plane-host>/auth/get-csrf-token/
# -> {"csrf_token": "<token>"}
# 2) sign in with NO User-Agent header (curl removes it when the value is empty)
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://<plane-host>/auth/sign-in/ \
-b cookies.txt -H 'User-Agent:' \
--data-urlencode 'email=<you@example.com>' \
--data-urlencode 'password=<password>' \
--data-urlencode 'csrfmiddlewaretoken=<token>'
# -> 500
The identical request with any User-Agent header succeeds (302 to the dashboard).
Expected behavior
Sign-in should succeed regardless of whether a User-Agent header is present. A
missing header should be treated as an empty string, not None.
Suggested fix
Default the header to an empty string in save_user_data, matching login.py:
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT", "")
(Alternatively make the field null=True, but the empty-string default is the
smaller change and keeps it consistent with the sibling code.)
Environment
- Plane self-hosted,
v1.4.1 (Docker Compose).
- Reproduced against the
plane-api service (makeplane/plane-backend:v1.4.1).
- Endpoint: the app auth API
POST /auth/sign-in/.
Describe the bug
POST /auth/sign-in/(email/password) returns an unhandled HTTP 500 when therequest does not send a
User-Agentheader. The failure happens after thecredentials are verified, so a client with correct credentials still cannot sign in.
Any programmatic client that omits a User-Agent hits this (for example a CLI/API
consumer; Rust's
reqwestsends no User-Agent by default).Root cause
AuthenticationAdapter.save_user_data()reads the header with no default:When the header is absent,
.get("HTTP_USER_AGENT")returnsNone. The field isdeclared:
blank=Truebut notnull=True, so the column isNOT NULL. SavingNoneraises anIntegrityError. It is not anAuthenticationException, so it is not caught by thesign-in view's handler and Django returns a generic 500.
The sibling code path already handles this correctly, which is the inconsistency:
To reproduce
Sign in with valid credentials but with the
User-Agentheader removed:The identical request with any
User-Agentheader succeeds (302 to the dashboard).Expected behavior
Sign-in should succeed regardless of whether a
User-Agentheader is present. Amissing header should be treated as an empty string, not
None.Suggested fix
Default the header to an empty string in
save_user_data, matchinglogin.py:(Alternatively make the field
null=True, but the empty-string default is thesmaller change and keeps it consistent with the sibling code.)
Environment
v1.4.1(Docker Compose).plane-apiservice (makeplane/plane-backend:v1.4.1).POST /auth/sign-in/.