{"record":{"id":"83cb1f6a14c5d1fc","repo":"v2fly/v2ray-core","slug":"no-inbound-metadata","errorCode":null,"errorMessage":"no inbound metadata","messagePattern":"no inbound metadata","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/trojan/server.go","lineNumber":188,"sourceCode":"\tclientReader := &ConnReader{Reader: bufferedReader}\n\tif err := clientReader.ParseHeader(); err != nil {\n\t\tlog.Record(&log.AccessMessage{\n\t\t\tFrom:   conn.RemoteAddr(),\n\t\t\tTo:     \"\",\n\t\t\tStatus: log.AccessRejected,\n\t\t\tReason: err,\n\t\t})\n\t\treturn newError(\"failed to create request from: \", conn.RemoteAddr()).Base(err)\n\t}\n\n\tdestination := clientReader.Target\n\tif err := conn.SetReadDeadline(time.Time{}); err != nil {\n\t\treturn newError(\"unable to set read deadline\").Base(err).AtWarning()\n\t}\n\n\tinbound := session.InboundFromContext(ctx)\n\tif inbound == nil {\n\t\tpanic(\"no inbound metadata\")\n\t}\n\tinbound.User = user\n\tsessionPolicy = s.policyManager.ForLevel(user.Level)\n\n\tif destination.Network == net.Network_UDP { // handle udp request\n\t\treturn s.handleUDPPayload(ctx, &PacketReader{Reader: clientReader}, &PacketWriter{Writer: conn}, dispatcher)\n\t}\n\n\tctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{\n\t\tFrom:   conn.RemoteAddr(),\n\t\tTo:     destination,\n\t\tStatus: log.AccessAccepted,\n\t\tReason: \"\",\n\t\tEmail:  user.Email,\n\t})\n\n\tnewError(\"received request for \", destination).WriteToLog(sid)\n\treturn s.handleConnection(ctx, sessionPolicy, destination, clientReader, buf.NewWriter(conn), dispatcher)","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/v2fly/v2ray-core/blob/db1291416195df07b287d79d8b9afe451c186c33/proxy/trojan/server.go#L170-L206","documentation":"This is a hard Go panic raised in the Trojan inbound's Process() after the client request has been successfully decoded. The handler expects the context to already carry session.Inbound metadata (set by the inbound worker in app/proxyman/inbound/worker.go via session.ContextWithInbound); if session.InboundFromContext(ctx) returns nil, the invariant 'this proxy is only driven by a registered inbound handler' was violated, so the process panics instead of continuing without inbound metadata.","triggerScenarios":"Calling trojan.Server.Process(ctx, ...) directly (e.g. from a test or a custom dispatcher) with a plain context.Background()/context.TODO() that never went through proxyman's worker; wiring the Trojan inbound into a custom transport or embedding that invokes the proxy without first doing ctx = session.ContextWithInbound(ctx, &session.Inbound{...}); any code path that rebuilds or copies the context and drops the inbound value before dispatch.","commonSituations":"Writing unit tests for proxy handlers and passing a bare context; forking the inbound pipeline or adding a new transport that bypasses app/proxyman/inbound/worker.go; refactoring that accidentally replaces ctx (e.g. ctx = context.Background()) before calling Process; using community forks that drive proxies through custom accept loops (tun, reverse bridge set inbound explicitly, custom code often forgets).","solutions":["Trace the caller: ensure the connection is accepted through a registered inbound handler (proxyman worker), which stamps ctx with session.ContextWithInbound before invoking the proxy.","If you invoke Process() yourself (tests/embedding), wrap the context first: ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: \"your-inbound-tag\"}).","Audit any intermediate code between accept and Process for context reassignment that drops values (search for 'ctx =' between the listener callback and the proxy call).","As a defensive measure in custom hosts, recover() the panic and log the offending goroutine path to identify the missing wiring."],"exampleFix":"// before (test / custom accept loop)\nfunc handle(conn net.Conn) {\n    ctx := context.Background()\n    go trojanServer.Process(ctx, link, conn) // panics: no inbound metadata\n}\n\n// after\nfunc handle(conn net.Conn) {\n    ctx := context.Background()\n    ctx = session.ContextWithInbound(ctx, &session.Inbound{\n        Tag: \"my-trojan-inbound\",\n        // normally filled by proxyman: Source/Gateway/Receiver as available\n    })\n    go trojanServer.Process(ctx, link, conn)\n}","handlingStrategy":"validation","validationCode":"// before driving a trojan inbound handler, verify the pipeline context:\nif session.InboundFromContext(ctx) == nil {\n    ctx = session.ContextWithInbound(ctx, &session.Inbound{\n        Tag:    \"my-inbound\",\n        Source: net.DestinationFromAddr(conn.RemoteAddr()),\n    })\n}\nerr := trojanInbound.Process(ctx, link, conn)","typeGuard":"func hasInboundMetadata(ctx context.Context) bool {\n    return session.InboundFromContext(ctx) != nil\n}","tryCatchPattern":"// last-resort containment around a nonstandard invocation:\nfunc safeProcess(ctx context.Context, p proxy.Inbound, r io.Reader, w io.Writer) (err error) {\n    defer func() {\n        if r := recover(); r != nil {\n            err = newError(\"inbound handler panicked: \", r)\n        }\n    }()\n    return p.Process(ctx, r, w)\n}","preventionTips":["Always accept inbound proxy connections through app/proxyman (registered inbounds) so session.ContextWithInbound is applied for you.","In custom entry points, copy the ContextWithInbound pattern from app/tun/handler_tcp.go or app/reverse/bridge.go.","Add a unit assertion that the context passed to Process contains inbound metadata before running handler tests.","Never replace ctx with a fresh context between listener accept and proxy.Process; derive from the worker-provided ctx instead."],"tags":["trojan","inbound","context","panic","session","integration"],"backgroundTag":null,"analyzedSha":"db1291416195df07b287d79d8b9afe451c186c33","analyzedAt":"2026-08-15T14:51:20.977Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}