signalapp/Signal-Server · warning · UnrecognizedUserAgentException
User-Agent string is blank
Error message
User-Agent string is blank
What it means
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.
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.
Example fix
// before curl -s https://server/api/v1/... # no User-Agent header -> blank // after curl -s -H "User-Agent: Signal-Android/7.0.0" https://server/api/v1/...
Defensive patterns
Strategy: try-catch
Validate before calling
// before calling
if (userAgentHeader == null || userAgentHeader.isBlank()) {
userAgent = UserAgentUtil.UNKNOWN_USER_AGENT; // or reject the request
} else {
userAgent = UserAgentUtil.parseUserAgentString(userAgentHeader);
} Type guard
boolean hasUserAgent(String ua) {
return ua != null && !ua.isBlank();
} Try / catch
UserAgent ua;
try {
ua = UserAgentUtil.parseUserAgentString(request.getHeader("User-Agent"));
} catch (UnrecognizedUserAgentException e) {
if (e.getMessage().contains("blank")) {
ua = UserAgentUtil.UNKNOWN_USER_AGENT; // or return 400 if UA is mandatory
} else {
ua = UserAgentUtil.UNKNOWN_USER_AGENT;
}
} Prevention
- 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
When it happens
Trigger: Calling UserAgentUtil.parseUserAgentString(null), parseUserAgentString(""), or parseUserAgentString(" ") — typically from maybeParseUserAgentString when a client sent no User-Agent header at all.
Common situations: 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.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- exceeded maximum uploadLength
- Invalid length
- Source object not found
- could not parse already validated number
- use websockets
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/dc652fda7096fa50.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtil.java:21
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util.ua;
import com.vdurmont.semver4j.Semver;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nullable;
public class UserAgentUtil {
private static final Pattern STANDARD_UA_PATTERN = Pattern.compile("^Signal-(Android|Desktop|iOS)/([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE);
public static UserAgent parseUserAgentString(final String userAgentString) throws UnrecognizedUserAgentException {
if (StringUtils.isBlank(userAgentString)) {
throw new UnrecognizedUserAgentException("User-Agent string is blank");
}
try {
final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString);
if (matcher.matches()) {
return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4)));
}
} catch (final Exception e) {
throw new UnrecognizedUserAgentException(e);
}
throw new UnrecognizedUserAgentException();
}
public static @Nullable UserAgent maybeParseUserAgentString(final String userAgentString) {
try {
return parseUserAgentString(userAgentString);View on GitHub (pinned to 100ab61c82)