signalapp/Signal-Server · error · IllegalArgumentException
PNI account identifier did not start with "PNI:" prefix
Error message
PNI account identifier did not start with "PNI:" prefix
What it means
PniServiceIdentifier.valueOf parses a "PNI:"-prefixed string into a PniServiceIdentifier and throws IllegalArgumentException if the string lacks the required "PNI:" prefix. This is Signal's guard ensuring PNI account identifiers carry their identity-type prefix before UUID parsing.
Solutions
- Normalize the string to include the "PNI:" prefix before parsing (e.g. if (!s.startsWith("PNI:")) s = "PNI:" + s)
- Parse raw UUIDs with new PniServiceIdentifier(UUID.fromString(...)) instead of valueOf
- Verify you are not accidentally passing an ACI identifier; use AciServiceIdentifier for ACI values
Example fix
// before
PniServiceIdentifier pni = PniServiceIdentifier.valueOf(uuidString); // no PNI: prefix
// after
String prefixed = uuidString.startsWith("PNI:") ? uuidString : "PNI:" + uuidString;
PniServiceIdentifier pni = PniServiceIdentifier.valueOf(prefixed); Defensive patterns
Strategy: try-catch
Validate before calling
if (s == null || !s.startsWith("PNI:")) throw new IllegalArgumentException("PNI identifier must start with PNI:"); Type guard
boolean isPniString(String s) { return s != null && s.startsWith("PNI:"); } Try / catch
try { pni = PniServiceIdentifier.valueOf(s); } catch (IllegalArgumentException e) { if (e.getMessage().contains("PNI:")) { /* handle missing prefix / wrong identifier type */ } } Prevention
- Always persist service identifiers with their ACI:/PNI: prefix
- Route parsing through typed valueOf methods matching the prefix
- Fail fast on identifier format at ingestion boundaries
When it happens
Trigger: Calling PniServiceIdentifier.valueOf() with a raw UUID string, an ACI-prefixed string ("ACI:..."), a bare hex UUID, or a string with wrong casing/whitespace before the prefix.
Common situations: Storing identifiers without their prefix in databases and re-parsing later; mixing ACI and PNI strings; user/config input containing unprefixed UUIDs; older persisted formats predating prefixes.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- could not parse already validated number
- Must specify a PNI-associated identity key when recovering…
- PNI identity key must not be provided if existing account…
- User-Agent string is blank
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/36cb5c3d5797334d.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifier.java:63
@Override
public byte[] toFixedWidthByteArray() {
final ByteBuffer byteBuffer = ByteBuffer.allocate(17);
byteBuffer.put(IDENTITY_TYPE.getBytePrefix());
byteBuffer.putLong(uuid.getMostSignificantBits());
byteBuffer.putLong(uuid.getLeastSignificantBits());
byteBuffer.flip();
return byteBuffer.array();
}
@Override
public ServiceId.Pni toLibsignal() {
return new ServiceId.Pni(uuid);
}
public static PniServiceIdentifier valueOf(final String string) {
if (!string.startsWith(IDENTITY_TYPE.getStringPrefix())) {
throw new IllegalArgumentException("PNI account identifier did not start with \"PNI:\" prefix");
}
return new PniServiceIdentifier(UUID.fromString(string.substring(IDENTITY_TYPE.getStringPrefix().length())));
}
public static PniServiceIdentifier fromBytes(final byte[] bytes) {
if (bytes.length == 17) {
if (bytes[0] != IDENTITY_TYPE.getBytePrefix()) {
throw new IllegalArgumentException("Unexpected byte array prefix: " + HexFormat.of().formatHex(new byte[] { bytes[0] }));
}
return new PniServiceIdentifier(UUIDUtil.fromBytes(Arrays.copyOfRange(bytes, 1, bytes.length)));
}
throw new IllegalArgumentException("Unexpected byte array length: " + bytes.length);
}
}
View on GitHub (pinned to 100ab61c82)