DEV Community

98IP Proxy
98IP Proxy

Posted on Fully Autonomous

Build a Host-and-Port Evidence Index for Proxy Debugging

Disclosure: I work with 98IP, a proxy infrastructure provider. This is a technical debugging pattern, not an independent product review.

A browser trace can contain hundreds of requests, but a useful proxy incident report may need only ten rows.

The difficult part is deciding which ten.

Modern applications split one user action across an application host, an API, identity endpoints, asset hosts, and sometimes alternate ports. If a proxied journey fails, a broad Network screenshot hides the first divergence. A narrow hostname filter can hide the redirect or token refresh that explains it.

The remedy is a small host-and-port evidence index: a deterministic table that preserves request order, classifies each upstream role, and separates browser observations from actual route proof.

Start with a request contract

Before opening DevTools, write the expected chain:

const expectedRoles = {
  document: ["app.example.test:443"],
  identity: ["auth.example.test:443"],
  api: ["api.example.test:443", "api.example.test:8443"],
  assets: ["static.example.test:443"]
};
Enter fullscreen mode Exit fullscreen mode

Use placeholders or approved internal values in shared examples. Do not paste real account paths, signed URLs, or proxy credentials into tickets.

This contract serves two purposes:

  1. It identifies an expected request that never happened.
  2. It prevents a successful asset request from being mistaken for evidence that the API route worked.

Capture the complete journey once

Open DevTools before navigation, enable Preserve log, clear old entries, and perform one authorized, non-destructive journey. Pin the request-number column when your browser supports it.

Do not immediately refresh five times. Repetition can trigger destination throttling, change caches, and replace the original causal sequence with a different incident.

Keep these fields visible:

  • request sequence;
  • hostname;
  • port;
  • method;
  • status class;
  • initiator;
  • protocol;
  • connection, TLS, TTFB, and total time where available.

The request number is a browser-observed sequence key, not a universal event ID. Parallel resource loading may change numbering between runs.

Normalize without retaining secrets

Here is a minimal JavaScript shape for a sanitized row:

function evidenceRow(flow, caseId) {
  const url = new URL(flow.url);

  return {
    case_id: caseId,
    request_number: flow.sequence,
    hostname: url.hostname,
    port: url.port || (url.protocol === "https:" ? "443" : "80"),
    method: flow.method,
    status_class: flow.status ? `${Math.floor(flow.status / 100)}xx` : "none",
    initiator_class: classifyInitiator(flow.initiator),
    failure_layer: "unclassified",
    route_verified: false,
    ttfb_ms: flow.timing?.ttfb ?? null,
    total_ms: flow.timing?.total ?? null
  };
}
Enter fullscreen mode Exit fullscreen mode

Notice what is missing: query strings, cookies, authorization headers, body data, and full paths. Those fields are rarely necessary for the first comparison and often contain the most sensitive information.

If a path is needed, retain a coarse route template such as /inventory/:id, not the literal identifier.

Use two axes: site relation and upstream identity

“Same-site” and “same host” are not synonyms.

A user journey can cross multiple hostnames that share a site boundary. It can also call a third-party identity or API service that users perceive as part of the product but the browser considers cross-site.

Add both fields:

{
  site_relation: "same-site", // or cross-site
  upstream_key: "api.example.test:8443"
}
Enter fullscreen mode Exit fullscreen mode

The site relation preserves browser security context. The upstream key identifies the actual host and port you must investigate.

Classify the first divergence

Compare a failing trace with one valid control where only one variable changed. Match requests by role, method, hostname, port, and route template—not by row position alone.

Then find the earliest meaningful difference:

function firstDivergence(control, failure) {
  const byKey = rows => new Map(rows.map(row => [
    [row.role, row.method, row.hostname, row.port, row.route_template].join("|"),
    row
  ]));

  const baseline = byKey(control);

  return failure.find(row => {
    const peer = baseline.get([
      row.role,
      row.method,
      row.hostname,
      row.port,
      row.route_template
    ].join("|"));

    return !peer ||
      peer.status_class !== row.status_class ||
      peer.failure_layer !== row.failure_layer;
  });
}
Enter fullscreen mode Exit fullscreen mode

Production comparison code should support repeated keys and redirects, but the principle is the same: a later 5xx may be only the visible symptom. The first useful difference might be an earlier authentication challenge, a failed preflight, or a request never emitted by the browser.

Keep failure layers explicit

Use a controlled vocabulary:

const failureLayers = [
  "browser",
  "service_worker",
  "dns",
  "proxy_gateway",
  "proxy_exit",
  "tls",
  "destination",
  "application",
  "policy",
  "unclassified"
];
Enter fullscreen mode Exit fullscreen mode

Do not label every timeout a “proxy failure.” A browser trace alone usually cannot prove the full route. Correlate it with a sanitized session identifier and an authorized route or exit check.

route_verified: false is a valid finding. It is better than converting an assumption into a conclusion.

Port differences deserve their own comparison

Two requests with the same hostname but different ports can reach different listeners, certificates, firewall rules, or proxy tunnel policies.

Compare the standard and alternate-port rows explicitly:

function groupByUpstream(rows) {
  return Object.groupBy(rows, row => `${row.hostname}:${row.port}`);
}
Enter fullscreen mode Exit fullscreen mode

If 443 succeeds and 8443 fails, do not conclude that “the domain works.” Test the layers in order:

  1. did the browser emit the request?
  2. did proxy authentication and tunneling complete?
  3. did TLS negotiate with the expected service?
  4. did the destination return the expected content class?
  5. was the intended route independently verified?

Export the minimum useful artifact

The default handoff should be:

  • the evidence index;
  • one screenshot showing sequence and host context;
  • a short reproduction contract;
  • a sanitized correlation identifier;
  • the first-divergence conclusion with uncertainty stated.

Only attach a full HAR when the recipient truly needs it and the archive has been reviewed for proxy authorization, account authorization, cookies, tokens, personal data, signed parameters, and sensitive response bodies.

A practical acceptance checklist

  • [ ] Test scope and target action are authorized.
  • [ ] Direct fallback is prevented.
  • [ ] Browser and DevTools versions are recorded.
  • [ ] Expected hosts and ports are listed before capture.
  • [ ] Preserve log begins before navigation.
  • [ ] The full site context is reviewed before narrowing.
  • [ ] Rows use a stable request-role key.
  • [ ] The first meaningful divergence is marked.
  • [ ] Browser observation and route proof are separate fields.
  • [ ] No credential, token, cookie, personal data, or unnecessary URL is retained.
  • [ ] Reproduction uses a safe, idempotent action.
  • [ ] Rate limits and destination restrictions stop the test.

The goal is not to collect more network data. It is to produce a smaller artifact that lets another engineer reach the same conclusion.

If you work on authorized proxy testing, 98IP publishes additional operational guides at https://en.98ip.com/?k=dev.

Top comments (0)