{"record":{"id":"29242b3584bc80fb","repo":"crowdsecurity/crowdsec","slug":"unable-to-parse-jwt-expiration-w-29242b","errorCode":null,"errorMessage":"unable to parse jwt expiration: %w","messagePattern":"unable to parse jwt expiration: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/apiclient/client.go","lineNumber":134,"sourceCode":"\t\tPapiURL:       papiURL,\n\t\tVersionPrefix: \"v1\",\n\t\tUpdateScenario: func(_ context.Context) ([]string, error) {\n\t\t\treturn scenarios, nil\n\t\t},\n\t})\n\n\tauthResp, _, err := client.Auth.AuthenticateWatcher(ctx, models.WatcherAuthRequest{\n\t\tMachineID: &login,\n\t\tPassword:  &pwd,\n\t\tScenarios: scenarios,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"authenticate watcher (%s): %w\", login, err)\n\t}\n\n\tvar expiration time.Time\n\tif err := expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {\n\t\treturn fmt.Errorf(\"unable to parse jwt expiration: %w\", err)\n\t}\n\n\tclient.GetClient().Transport.(*JWTTransport).Token = authResp.Token\n\tclient.GetClient().Transport.(*JWTTransport).Expiration = expiration\n\n\tlapiClient = client\n\n\treturn nil\n}\n\nfunc GetLAPIClient() (*ApiClient, error) {\n\tif lapiClient == nil {\n\t\treturn nil, errors.New(\"client not initialized\")\n\t}\n\n\treturn lapiClient, nil\n}\n","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/apiclient/client.go#L116-L152","documentation":"After a successful watcher authentication, the server-issued expiration timestamp (authResp.Expire) is parsed with time.Time.UnmarshalText, which requires RFC3339 format. If the server returned an empty or malformed expiration string, parsing fails and InitLAPIClient aborts even though the token itself was obtained.","triggerScenarios":"expiration.UnmarshalText([]byte(authResp.Expire)) fails because authResp.Expire is empty, null, or not in RFC3339 format (e.g. '2026-09-06 12:00:00' without T/timezone, or a non-date string).","commonSituations":"LAPI behind a proxy that mangles the JSON response; version mismatch between client and server returning a different date format; auth endpoint hit through a wrong path returning HTML instead of the expected JSON; a mocked/test server returning incomplete WatcherAuthResponse.","solutions":["Check what the LAPI actually returns for the auth endpoint (curl -X POST .../watchers/login) and inspect the 'expirations'/'expire' field format","Align crowdsec client and LAPI server versions; a format change between versions can cause this","If a proxy is involved, verify it passes the JSON body through unmodified","Check that the response is real JSON from LAPI and not an HTML error page (a 200 with wrong body decodes into an empty Expire)"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// after a successful raw auth call, check the expiration field before parsing\nif authResp.Expire == \"\" {\n    return errors.New(\"server returned empty jwt expiration\")\n}\nif _, err := time.Parse(time.RFC3339, authResp.Expire); err != nil {\n    return fmt.Errorf(\"unexpected expiration format %q: %w\", authResp.Expire, err)\n}","typeGuard":"func hasValidExpire(resp *models.WatcherAuthResponse) bool {\n    if resp == nil || resp.Expire == \"\" {\n        return false\n    }\n    _, err := time.Parse(time.RFC3339, resp.Expire)\n    return err == nil\n}","tryCatchPattern":null,"preventionTips":["Keep crowdsec client and LAPI server versions aligned","Ensure no proxy rewrites or truncates the auth JSON response","Verify the auth endpoint returns application/json, not an HTML error page"],"tags":["jwt","date-format","api-response"],"backgroundTag":"invalid-date-format","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}