{"record":{"id":"dc652fda7096fa50","repo":"signalapp/Signal-Server","slug":"user-agent-string-is-blank","errorCode":null,"errorMessage":"User-Agent string is blank","messagePattern":"User-Agent string is blank","errorType":"validation","errorClass":"UnrecognizedUserAgentException","httpStatus":null,"severity":"warning","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtil.java","lineNumber":21,"sourceCode":" * SPDX-License-Identifier: AGPL-3.0-only\n */\n\npackage org.whispersystems.textsecuregcm.util.ua;\n\nimport com.vdurmont.semver4j.Semver;\nimport java.util.Optional;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.apache.commons.lang3.StringUtils;\nimport javax.annotation.Nullable;\n\npublic class UserAgentUtil {\n\n  private static final Pattern STANDARD_UA_PATTERN = Pattern.compile(\"^Signal-(Android|Desktop|iOS)/([^ ]+)( (.+))?$\", Pattern.CASE_INSENSITIVE);\n\n  public static UserAgent parseUserAgentString(final String userAgentString) throws UnrecognizedUserAgentException {\n    if (StringUtils.isBlank(userAgentString)) {\n      throw new UnrecognizedUserAgentException(\"User-Agent string is blank\");\n    }\n\n    try {\n      final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString);\n\n      if (matcher.matches()) {\n        return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4)));\n      }\n    } catch (final Exception e) {\n      throw new UnrecognizedUserAgentException(e);\n    }\n\n    throw new UnrecognizedUserAgentException();\n  }\n\n  public static @Nullable UserAgent maybeParseUserAgentString(final String userAgentString) {\n    try {\n      return parseUserAgentString(userAgentString);","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtil.java#L3-L39","documentation":"parseUserAgentString throws UnrecognizedUserAgentException with \"User-Agent string is blank\" when the incoming User-Agent header is null, empty, or whitespace-only. Signal-Server only recognizes User-Agent strings matching the pattern Signal-(Android|Desktop|iOS)/<version>; a blank string cannot even be attempted against the pattern, so it fails fast with a dedicated exception.","triggerScenarios":"Calling UserAgentUtil.parseUserAgentString(null), parseUserAgentString(\"\"), or parseUserAgentString(\"   \") — typically from maybeParseUserAgentString when a client sent no User-Agent header at all.","commonSituations":"Curl/HTTP client calls made without a User-Agent header; stripped headers by load balancers, proxies, or privacy tooling; internal health checks and service-to-service gRPC/HTTP calls that omit the header; bot or script traffic.","solutions":["Have the client send a compliant User-Agent, e.g. \"Signal-Android/7.0.0\", \"Signal-Desktop/6.30.0\", or \"Signal-iOS/7.0.5\".","If blank UAs are acceptable in your context, call maybeParseUserAgentString (or catch UnrecognizedUserAgentException) and fall back to a default/unknown UserAgent.","Check proxy/load-balancer config so the User-Agent header is forwarded rather than stripped.","For scripts/tests, set the header explicitly: -H \"User-Agent: Signal-Desktop/6.30.0\".","Treat the exception as a signal to request a client update if your service requires version reporting."],"exampleFix":"// before\ncurl -s https://server/api/v1/...           # no User-Agent header -> blank\n// after\ncurl -s -H \"User-Agent: Signal-Android/7.0.0\" https://server/api/v1/...","handlingStrategy":"try-catch","validationCode":"// before calling\nif (userAgentHeader == null || userAgentHeader.isBlank()) {\n    userAgent = UserAgentUtil.UNKNOWN_USER_AGENT; // or reject the request\n} else {\n    userAgent = UserAgentUtil.parseUserAgentString(userAgentHeader);\n}","typeGuard":"boolean hasUserAgent(String ua) {\n    return ua != null && !ua.isBlank();\n}","tryCatchPattern":"UserAgent ua;\ntry {\n    ua = UserAgentUtil.parseUserAgentString(request.getHeader(\"User-Agent\"));\n} catch (UnrecognizedUserAgentException e) {\n    if (e.getMessage().contains(\"blank\")) {\n        ua = UserAgentUtil.UNKNOWN_USER_AGENT; // or return 400 if UA is mandatory\n    } else {\n        ua = UserAgentUtil.UNKNOWN_USER_AGENT;\n    }\n}","preventionTips":["Always send a Signal-standard UA header: Signal-<Platform>/<version>","Use maybeParseUserAgentString when a missing UA is an expected, non-fatal case","Verify proxies/load balancers forward the User-Agent header","Add the header to health checks, scripts, and integration tests"],"tags":["user-agent","http-headers","parsing","signal"],"backgroundTag":"empty-required-field","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}