URL Percent-Encoding, Query Parsing, and Unix Timestamps in API Debugging

By Huzaifa • Published: 2026-09-16 • 6 min read

Untangle broken query parameters, understand encodeURI vs. encodeURIComponent vs. URLSearchParams, and avoid seconds-versus-milliseconds Unix epoch bugs across APIs and webhooks.

URL Percent-Encoding, Query Parsing, and Unix Timestamps in API Debugging cover image

A surprising share of production API bugs come down to two deceptively simple data representations: how parameters are encoded inside URLs, and how points in time are represented across client and server boundaries.

A single unencoded & or + inside a search filter can silently truncate a query parameter, while mixing up 10-digit Unix seconds with 13-digit JavaScript milliseconds can expire sessions immediately or schedule a background job for the year 58,494. Understanding the underlying standards makes both classes of bugs trivial to spot and prevent.

Why RFC 3986 Percent-Encoding Exists

Under RFC 3986, a Uniform Resource Identifier (URI) uses specific ASCII characters as structural delimiters:

  • : and / separate the scheme, authority, and path segments.
  • ? begins the query string.
  • & and = separate key-value pairs inside query parameters.
  • # introduces a client-side fragment identifier (which browsers never send to the HTTP server!).

Whenever user input or data inside a parameter contains one of those reserved characters—or spaces, non-ASCII UTF-8 characters, or symbols like %—each UTF-8 byte must be converted into % followed by two hexadecimal digits (for example, %20 for a space, %26 for &, and %2B for +).

Suppose a user searches for C++ & Rust and naive frontend code concatenates strings directly:

Broken:   /api/search?q=C++ & Rust&limit=10
Server reads:
  q     = "C   "   (because + decodes to space in form-urlencoded queries, and & split the param!)
  Rust  = ""
  limit = "10"

Properly percent-encoding the parameter value keeps the entire string intact:

Correct:  /api/search?q=C%2B%2B%20%26%20Rust&limit=10

When inspecting raw webhook callback URLs, OAuth redirect_uri strings, or signed cloud storage links, paste them into the URL Encoder & Decoder to encode or decode values cleanly, or drop the full URL into the URL Parser & Inspector to break out its protocol, hostname, port, pathname, individual query parameters, and hash fragment side by side.

encodeURI vs. encodeURIComponent vs. URLSearchParams

JavaScript provides two global encoding functions whose names confuse almost everyone at least once:

  1. encodeURI(fullUrl) assumes you are passing a complete URL and intentionally does NOT encode :, /, ?, &, =, +, or #. If you pass a query parameter value into encodeURI(), any &, =, or + inside the value stays unescaped and corrupts your query string!
  2. encodeURIComponent(paramValue) encodes everything except unreserved characters (A–Z a–z 0–9 - _ . ! ~ * ' ( )), escaping &, =, +, /, ?, and #. Whenever you interpolate a single path segment or query value manually, encodeURIComponent is the function you want.
  3. new URL() and URLSearchParams (Best Practice): Modern browsers and Node.js let you construct URLs without manual string concatenation at all:
const endpoint = new URL('https://api.example.com/v1/articles');
endpoint.searchParams.set('q', 'C++ & Rust');
endpoint.searchParams.set('page', '2');

console.log(endpoint.toString());
// "https://api.example.com/v1/articles?q=C%2B%2B+%26+Rust&page=2"

One Subtle Detail: %20 vs. + for Spaces

Notice how URLSearchParams encodes spaces as + (following the application/x-www-form-urlencoded specification used by HTML forms), whereas encodeURIComponent encodes spaces as %20 (following strict RFC 3986 URI encoding). Almost all web servers accept + as a space inside query strings, except certain strict cryptographic signature schemes (such as AWS Signature v4 canonical query strings or OAuth 1.0a), which require %20. Also beware of double encoding: calling encodeURIComponent on an already-encoded string turns %20 into %2520!

Unix Epoch Timestamps: Seconds vs. Milliseconds

Once your URL arrives at an API—especially analytics endpoints, payment webhooks, or JWT verifiers—time filters and expiration fields are usually passed as Unix epoch timestamps or ISO 8601 strings.

The Unix epoch counts elapsed UTC seconds since 1970-01-01T00:00:00Z (ignoring leap seconds). The #1 timestamp bug across stacks is unit mismatch:

  • 10 digits = Seconds (for example, 1792659600): Standard in Unix/POSIX time(), Python int(time.time()), Go time.Now().Unix(), PHP time(), Stripe webhooks, and JWT exp / iat / nbf claims.
  • 13 digits = Milliseconds (for example, 1792659600000): Standard in JavaScript Date.now(), new Date(ms), and Java System.currentTimeMillis().
  • 16 or 19 digits = Microseconds or Nanoseconds: Common in high-resolution observability traces and InfluxDB / OpenTelemetry logs.

Look at what happens in JavaScript if you pass a 10-digit JWT exp directly into new Date():

const jwtExpSeconds = 1792659600;

// BUG: JavaScript interprets the number as milliseconds after Jan 1, 1970!
console.log(new Date(jwtExpSeconds).toISOString());
// "1970-01-21T17:57:39.600Z"

// CORRECT: Multiply seconds by 1000 for JavaScript Date
console.log(new Date(jwtExpSeconds * 1000).toISOString());
// "2026-10-22T09:00:00.000Z"

Whenever you see an unfamiliar integer timestamp in a database row, webhook header, or log trace, paste it into the Timestamp Converter to inspect both its UTC (ISO 8601) representation and your local timezone equivalent immediately.

Practical Rules for Reliable APIs

  1. Store and transmit UTC on the wire: Always store timestamps in UTC on the backend, and return unambiguous ISO 8601 UTC strings with a trailing Z (such as "2026-10-22T09:00:00.000Z")—or explicitly name integer fields with their unit (expiresAtSeconds vs createdAtMs) so callers never have to guess.
  2. Convert to local time only at the UI layer: Use Intl.DateTimeFormat in the browser right before rendering dates for human readers.
  3. Remember the Year 2038 boundary for signed 32-bit integers: Systems storing Unix seconds in a signed 32-bit integer (INT in SQL) overflow on 2038-01-19T03:14:07Z (2147483647). Always use 64-bit integers (BIGINT) or native TIMESTAMPTZ columns for long-term dates, mortgages, or certificate lifetimes.
  4. Preserve URL fragments and repeated query keys intentionally: Remember that URLSearchParams.get('tag') returns only the first value when a URL repeats ?tag=security&tag=http, whereas URLSearchParams.getAll('tag') returns the full array ['security', 'http']. Checking multi-value parameters in the URL Parser & Inspector and testing special characters in the URL Encoder & Decoder catches subtle serialization mismatches before they reach production.

Featured Tools Mentioned in This Guide

  • URL Encoder & Decoder — Percent-encode or decode URL query parameters, special characters, and full URI paths securely.
  • URL Parser & Inspector — Deconstruct URIs into protocol, host, pathname, query parameters, and hash fragments.
  • Timestamp Converter — Convert Unix epoch timestamps to human-readable UTC and local dates instantly.