DEV Community

xbb
xbb

Posted on

Normalizing TikTok handles is messier than it looks

Every tool that takes a TikTok username as input ends up writing the same 30 lines. Users paste whatever is in their clipboard, and "whatever" turns out to be about six different shapes:

charli
@charli
CHARLI
https://www.tiktok.com/@charli
tiktok.com/@charli/video/7300000000000000000
https://tiktok.com/@charli?lang=en&is_from_webapp=1
Enter fullscreen mode Exit fullscreen mode

All six mean the same account. None of them are equal as strings.

The order of operations matters

The trap is doing these steps in the wrong order. If you strip the @ first, a URL like tiktok.com/@charli?lang=en leaves you with charli?lang=en. If you split on ? last, the query string is still glued to the handle when you validate it.

The order that actually works:

  1. If the string contains tiktok.com/, keep only what follows it.
  2. Cut everything from the first ? or #.
  3. Trim slashes, then keep only the first path segment (this drops /video/123).
  4. Now strip the leading @.
  5. Validate, then lowercase.

In Go that is short enough to read in one sitting:

var valid = regexp.MustCompile(`^[A-Za-z0-9._]{2,24}$`)

func Normalize(in string) (string, error) {
    s := strings.TrimSpace(in)
    if i := strings.Index(s, "tiktok.com/"); i >= 0 {
        s = s[i+len("tiktok.com/"):]
    }
    if i := strings.IndexAny(s, "?#"); i >= 0 {
        s = s[:i]
    }
    s = strings.Trim(s, "/")
    if i := strings.Index(s, "/"); i >= 0 {
        s = s[:i]
    }
    s = strings.TrimPrefix(s, "@")
    if !valid.MatchString(s) {
        return "", ErrInvalid
    }
    return strings.ToLower(s), nil
}
Enter fullscreen mode Exit fullscreen mode

Two things worth knowing about the charset

TikTok handles allow letters, digits, underscores and periods — no hyphens, which surprises people coming from GitHub. The length range is 2 to 24. And they are case-insensitive for lookup but preserve case for display, so lowercasing for your cache key is safe and lowercasing for display is not.

Watch the period specifically: char.li and charli are different accounts, so you cannot normalize dots away the way you would with a Gmail address.

Why I ended up writing this

I built TikViewer, a browser tool where you type a TikTok username and watch that account's stories without showing up in their viewer list, and the single most common support question was "it says user not found" from someone who had pasted a full share URL with tracking params. Fixing the parser fixed the bug reports.

The extracted version, with tests, is here if it is useful to anyone: jacky-xbb/tikviewer-cli.

$ tikviewer "https://www.tiktok.com/@Charli/video/123?lang=en"
https://tikviewer.org/@charli
Enter fullscreen mode Exit fullscreen mode

If your own input parsing has a case I missed, I would genuinely like to hear it.

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

The order-of-operations trap is real and platform-specific in ways that hurt. We ingest usernames pasted from screenshots, bios and clipboards for a dozen platforms behind one normalization endpoint, and the failure mode was never the regex — it was the pipeline order, exactly like your '@'-strip-first example. We now keep one canonical pipeline per platform (scheme → host → query/fragment → path segment → sigil → charset/length → case) and unit-test each stage separately, because the combined function is easy to write and impossible to review.

The dot rule caught my eye: same class of footgun as email local-part normalization, except reversed. With Gmail you may collapse dots, with TikTok you must not — and if you cache by the lowercased display string, the two mistakes look identical in production: a silent wrong-profile hit. Did you add a test vector for 'char.li' vs 'charli' specifically, or is that still aspirational?

We also learned to reject rather than repair ambiguous input (a bare "charli" with no platform context). Guessing right 95% of the time sounds fine until the 5% are public wrong-account links.