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

  1. Normalize the string to include the "PNI:" prefix before parsing (e.g. if (!s.startsWith("PNI:")) s = "PNI:" + s)
  2. Parse raw UUIDs with new PniServiceIdentifier(UUID.fromString(...)) instead of valueOf
  3. 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

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


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)