{"record":{"id":"19cbff0db401bf63","repo":"SonarSource/sonarqube","slug":"gitlab-returned-an-unparseable-token-expiry","errorCode":null,"errorMessage":"GitLab returned an unparseable token expiry '{}', falling back to the requested date","messagePattern":"GitLab returned an unparseable token expiry '(.+?)', falling back to the requested date","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabScmAccessTokenProvider.java","lineNumber":202,"sourceCode":"  private static Long parseGitlabProjectId(@Nullable String almRepo, String safeProjectKey) {\n    if (almRepo == null || almRepo.isBlank()) {\n      LOG.warn(\"Cannot mint a GitLab access token: project '{}' has no repository configured on its DevOps Platform binding\", safeProjectKey);\n      return null;\n    }\n    try {\n      return Long.parseLong(almRepo);\n    } catch (NumberFormatException e) {\n      LOG.warn(\"Cannot mint a GitLab access token: project '{}' has a non-numeric GitLab repository identifier '{}'\", safeProjectKey, sanitizeForLog(almRepo));\n      return null;\n    }\n  }\n\n  private static String formatExpiresAt(@Nullable String responseExpiresAt, LocalDate requestedExpiresAt) {\n    if (responseExpiresAt != null && !responseExpiresAt.isBlank()) {\n      try {\n        return LocalDate.parse(responseExpiresAt.trim()).format(DateTimeFormatter.ISO_LOCAL_DATE);\n      } catch (DateTimeParseException e) {\n        LOG.warn(\"GitLab returned an unparseable token expiry '{}', falling back to the requested date\", sanitizeForLog(responseExpiresAt));\n      }\n    }\n    return requestedExpiresAt.format(DateTimeFormatter.ISO_LOCAL_DATE);\n  }\n\n  private static String sanitizeForLog(String value) {\n    return CRLF_PATTERN.matcher(value).replaceAll(\"_\");\n  }\n\n  private record TokenCacheKey(String projectUuid, String almSettingUuid, long gitlabProjectId, long almSettingUpdatedAt) {\n  }\n\n  private record TokenMintRequest(TokenCacheKey cacheKey, String safeProjectKey, AlmSettingDto almSetting) {\n  }\n}\n","sourceCodeStart":184,"sourceCodeEnd":218,"githubUrl":"https://github.com/SonarSource/sonarqube/blob/184c821202192afc1c599fc912d0889b69fffa53/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabScmAccessTokenProvider.java#L184-L218","documentation":"formatExpiresAt() parses the expiry date returned by GitLab's token-creation API response. If the date string is present but not parseable as an ISO local date (DateTimeParseException), it logs this warning and falls back to the locally requested expiry date. Token creation continues; only the recorded expiry is approximated.","triggerScenarios":"GitLab API returns an expires_at value in an unexpected format (null-safe but non-ISO, e.g. with timezone suffix or locale formatting), typically due to GitLab version differences or a proxy altering the response.","commonSituations":"Mixed GitLab versions behind API gateways; custom GitLab plugins modifying responses; clock/date format changes between GitLab releases.","solutions":["No immediate action required — the provider falls back to the requested date and minting succeeds.","Check the GitLab instance version and response format of POST /api/v4/personal_access_tokens; upgrade GitLab if it emits non-ISO dates.","Inspect any middleware/proxy that might rewrite JSON fields.","Verify the fallback date is acceptable; if not, correct the requested expiry passed to the API."],"exampleFix":"// before (GitLab returns)\nexpires_at: \"09/09/2026\" // unparseable\n// after (fix GitLab output or accept fallback)\nexpires_at: \"2026-09-09\" // ISO_LOCAL_DATE, parsed normally","handlingStrategy":"fallback","validationCode":"// Java\ntry { LocalDate.parse(gitlabExpiresAt.trim()); }\ncatch (DateTimeParseException e) { /* use locally requested expiry as fallback */ }","typeGuard":"Optional<LocalDate> tryParseIsoDate(String value) {\n  if (value == null || value.isBlank()) return Optional.empty();\n  try { return Optional.of(LocalDate.parse(value.trim())); }\n  catch (DateTimeParseException e) { return Optional.empty(); }\n}","tryCatchPattern":"try {\n  LocalDate d = LocalDate.parse(responseExpiresAt.trim());\n} catch (DateTimeParseException e) {\n  LOG.warn(\"Unparseable expiry '{}', using requested date\", responseExpiresAt);\n  LocalDate d = requestedExpiresAt;\n}","preventionTips":["Keep GitLab on a version that returns ISO-8601 expires_at.","Check proxies/middleware for JSON response rewriting.","Treat this warning as informational; verify fallback dates are sane.","Add a test asserting the date-format contract in integration tests."],"tags":["gitlab","date-format","api-response","fallback"],"backgroundTag":"invalid-date-format","analyzedSha":"184c821202192afc1c599fc912d0889b69fffa53","analyzedAt":"2026-09-09T12:23:51.573Z","contentChangedAt":"2026-09-09T12:23:51.573Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}