Skip to content

JA4H

JA4H fingerprints one HTTP request. The value describes the shape of the request, and it reads no header value except the cookie fields.

The value

One JA4H value holds four parts, and three underscores separate them. R1 of docs/specs/foxio/JA4H.md holds the reading, and the image example is ge20cr13enus_974ebe531c03_b66fa821d02c_e97928733c74.

Part What it holds Rules
a The method code, the HTTP version, the cookie character, the referer character, the header count and four Accept-Language characters. R3 to R17
b A truncated hash of the header names, in wire order. R18 to R20
c A truncated hash of the cookie field names, sorted. R21 and R22
d A truncated hash of the cookie name=value pairs, sorted. R23 and R24

Part a

The method code takes two characters, and the HTTP version takes two characters. R3 and R5 hold the two fields. The cookie character is c when the request carries a cookie, and the referer character is r when it carries a referer. R6 and R7 hold the two, and each one writes n otherwise.

The header count takes two characters, and it counts no Cookie header and no Referer header. R8 quotes the FoxIO text specification:

2 digit number of headers, not counting Cookie and Referer. For 3 headers the value is "03".

R9 quotes the cap:

If there are more than 99, the output is 99.

The count also excludes an HTTP/2 pseudo-header, under R11, and the image states no such rule.

The four Accept-Language characters read the primary value, which is the text before the first comma. R12 and R13 hold it. The field drops a hyphen and it lowercases a capital letter, under R14. A value shorter than four characters pads with a trailing 0, under R15, and a request with no such header writes 0000 under R16.

Part b, part c and part d

Each hash is a SHA-256 truncated to twelve characters, under R26, and a comma separates each pair of list values under R25.

Part b reads header names alone, and it reads no header value. R20 holds it, and R18 states that the list keeps the wire order.

Part c and part d each sort their list. R22 holds part c, and R23 quotes the image label of part d: Truncated SHA256 hash of Cookie Fields + Values, sorted. R24 holds the entry form of part d, which writes the field name, then =, then the value. A request that carries no cookie writes 000000000000 in part c and in part d, under R27, and the image states no such rule.

What this library emits

JA4HFingerprinter produces the value. NewJA4H builds one, and ProcessPacket reads one packet. ComputeJA4H reads one packet and returns one value.

One complete HTTP request produces one result. The fingerprinter reads a request that one packet carries, and it reassembles a request that spans several TCP segments. The maintainer ruled the emission point at issue #455, and parser.HTTPMessageIsComplete holds the rule: the value lands when the request is complete, and not when the header block ends.

The result carries three forms of the fingerprint.

Field What it holds
Fingerprint The JA4H value.
Raw JA4H_r, the unhashed form with the sorted cookie lists.
RawOriginalOrder JA4H_ro, the unhashed form with the wire-order cookie lists.

The Type field holds ja4h.

Two questions that the FoxIO implementations split on

The method code of an unusual method. R3 and R4 record four different answers. The Wireshark dissector holds a table of 43 methods, the Zeek package holds a table of nine, and the reference Python lowercases the first two characters of any method.

The non-alphabetic Accept-Language character. R17 records two answers.

Part b of a request that carries no header

The maintainer ruled this split on 2026-08-14. Part b hashes an empty header list to e3b0c44298fc, and it writes no zero sentinel.

The four FoxIO implementations split two against two, read at the pinned commit. The reference Python and the Wireshark dissector each hash the empty string and write e3b0c44298fc. The reference Rust and the Zeek package each write the zero sentinel 000000000000. R18 of the JA4H transcription names no sentinel for part b, and R27 confines the sentinel to part c and to part d.

computeJA4HFromRequest in ja4h.go calls parser.TruncatedHashNoSentinel for part b, and it calls parser.TruncatedHash for part c and for part d. So one request can carry e3b0c44298fc in part b and 000000000000 in part c.

Issue #527 is the reversal path, and the port half is Crank-Git/ja4plus#612.

Where the register records a difference

testdata/deviations.json holds 36 entries under JA4H.1, 104 under JA4H_r.1 and 110 under JA4H_ro.1, measured on this branch. Two rulings cover them.

Key Ruling #441 Ruling #285
JA4H.1 36 0
JA4H_r.1 36 68
JA4H_ro.1 36 74

Ruling #441 records a capability decline.

467 records ruling #441, and the library reads TCP alone as testdata/foxio/reference/python/ja4.py:514 does, so this SSDP request over UDP produces no value.

Ruling #285 records a disagreement between the two FoxIO vector sets.

The two FoxIO vector sets disagree on the JA4H shape, and the maintainer ruled on 2026-08-12 that this library follows the per-stream set.