Skip to content

t3 serve sends GET / every ~16 seconds to every port opened by a process an agent started #29087

Closed

Description

@dgregorybm

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In a T3 Code thread, ask the agent to start a listener on a random local
    port. For example:

    import http.server, socketserver, time
    class H(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            print(time.strftime("%X"), self.command, self.path, dict(self.headers), flush=True)
            self.send_response(404); self.send_header("Content-Length", "0"); self.end_headers()
    s = socketserver.ThreadingTCPServer(("127.0.0.1", 0), H)
    print("listening on", s.server_address[1], flush=True)
    s.serve_forever()
  2. Don't open a preview, and don't point any client at the port.

  3. Watch the listener's output for a minute or two.

The listener doesn't have to be a dev server, or anything the user means to
preview. It can be a child of anything the agent runs, such as an HTTP fake
inside a test suite.

Expected behavior

No requests arrive. Starting a process isn't asking for a preview of it, and
nothing outside the user's own tools should be talking to the port.

If T3 needs to detect ports for previews, it should:

  • only probe when the user opens or asks for a preview;
  • offer a setting to turn automatic port detection off;
  • rely on the lsof/netstat listener scan it already runs, and not send
    HTTP at all. Where an HTTP check is unavoidable, use a dedicated user agent
    such as T3Code-PortProbe/<version>, so servers can recognise and ignore it;
  • back off on a port that has answered 404, rather than retrying every 16
    seconds indefinitely.

Actual behavior

Requests arrive about 1.6 s, 17.3 s and 33.0 s after the port opens, and then
every ~16 s for as long as the port stays open. Each one looks like this:

GET / HTTP/1.1
host: localhost:<port>
connection: keep-alive
b3: <trace id>-<span id>-1-<parent span id>
traceparent: 00-<trace id>-<span id>-01
accept: */*
accept-language: *
sec-fetch-mode: cors
user-agent: node
accept-encoding: gzip, deflate

The same trace id is reused across every probe and every port. In one
observation window, eight idle ports each got 6 probes in 90 seconds, 48
requests in all. Running lsof on the connecting socket's source port while a
probe was in flight showed it belonged to the t3 serve process.

Which ports get probed: the server's PortDiscovery lists listeners with
lsof -iTCP -sTCP:LISTEN every 3 seconds, while a client is subscribed to
discovered local servers. It then probes over HTTP only the listeners it
counts as owned. A listener is owned if it was started in a T3 terminal, or if
the listening process has T3CODE_THREAD_ID in its environment (read with
ps -E). Every command an agent runs inherits that variable, and so does every
child process those commands start. I tested this by running two listeners
side by side from an agent's shell for 40 seconds. The one with the variable
was probed; the one started under env -u T3CODE_THREAD_ID was not. A
listener started from a plain Terminal window doesn't have the variable either.

Any local server that logs, counts or reacts to requests sees these as real
traffic:

  • Test suites: fake HTTP servers that record every request fail at
    random, because a probe landing mid-test is a request the test never made.
    In one project, every full run of a ~960-test suite failed a different 1–4
    tests until the fixtures were taught to ignore the probe (see Workaround).
  • Dev servers: the probes show up in access logs, request counters,
    hot-reload logs and rate limiters.
  • Side effects: a server whose GET / does something, such as a one-shot
    OAuth callback listener or a webhook receiver, has it triggered.

Impact

Major degradation or frequent failure

Version or commit

0.1.5

Environment

macOS 27.0.1

Logs or stack traces

Screenshots, recordings, or supporting files

No response

Workaround

There is no setting to turn the probing off. Settings has no port-detection
entry, and the server's discovery code reads no setting before it probes.

There are two workarounds:

  1. Start the process without T3's thread id, so T3 doesn't count its
    listeners as owned:

    env -u T3CODE_THREAD_ID pytest

    This stops the probes completely. But it only works for commands you can
    wrap, and anything else T3 uses that variable for no longer sees the
    process.

  2. Recognise the probe in the server and ignore it. A probe is a GET /
    whose user-agent is exactly node. Match the header name
    case-insensitively, because Node sends every header name in lower case,
    and some servers' header lookups are case-sensitive. Answer the probe with
    an empty 404 and leave it out of whatever the server logs or records. For
    example, in a Python http.server handler:

    def is_port_probe(handler) -> bool:
        headers = {k.lower(): v for k, v in handler.headers.items()}
        return handler.command == "GET" and handler.path == "/" \
            and headers.get("user-agent") == "node"

    This has limits:

    • It only helps servers you control. Third-party dev servers still log and
      count the probes.
    • It also ignores any real client that sends GET / as
      user-agent: node, such as a bare fetch() from a Node script.
    • It depends on the probe's current headers, so it breaks if T3 changes
      them.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Nov 2, 2026
  2. juliusmarminge commented on Nov 2, 2026

    @juliusmarminge
    Member

    Thanks for the detailed report. This is fixed on main by bf8e132 (fix(server): preview discovery stops sending HTTP and TLS to agent-started ports, https://git.cafe/pingdotgg/t3code/pulls/27893), which first shipped in v0.1.6. Your 0.1.5 build predates it.

    Preview discovery still lists listeners it attributes to a thread, through the same lsof / /proc scan and the T3CODE_THREAD_ID environment. It no longer sends them any request. The only URLs it still requests are preview URLs you configured yourself.

    What I ran, on Linux with a fresh dev database for each build. A thread's agent started your listener script, with the thread details panel open so a client was subscribed to discovered servers:

    • v0.1.5: the agent-started listener got GET / with user-agent: node and the same trace id at +0.4 s, +16.0 s and +31.5 s, then every ~15.5 s. That was 7 requests in 94 s, matching your capture.
    • main (333f7a6): two listeners carried the thread's T3CODE_THREAD_ID. One was started by the agent; the other was a grandchild process that inherited the variable, like a test-suite HTTP fake. Both appeared under Servers as "Agent · python3". Neither got a single request in ~2 minutes.

    The regression test discovers owned listeners without sending HTTP or TLS probes in apps/server/src/preview/PortScanner.test.ts covers this. Updating to v0.1.6 or later should stop the probes, so I'm closing this. Reply if you still see them on a current build.

  3. dgregorybm commented on Nov 2, 2026

    @dgregorybm
    Author

    Thank you Julius! Sorry for the unnecessary report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions