v2fly/v2ray-core · error
no inbound metadata
Error message
no inbound metadata
What it means
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.
Source
Thrown at proxy/trojan/server.go:188
clientReader := &ConnReader{Reader: bufferedReader}
if err := clientReader.ParseHeader(); err != nil {
log.Record(&log.AccessMessage{
From: conn.RemoteAddr(),
To: "",
Status: log.AccessRejected,
Reason: err,
})
return newError("failed to create request from: ", conn.RemoteAddr()).Base(err)
}
destination := clientReader.Target
if err := conn.SetReadDeadline(time.Time{}); err != nil {
return newError("unable to set read deadline").Base(err).AtWarning()
}
inbound := session.InboundFromContext(ctx)
if inbound == nil {
panic("no inbound metadata")
}
inbound.User = user
sessionPolicy = s.policyManager.ForLevel(user.Level)
if destination.Network == net.Network_UDP { // handle udp request
return s.handleUDPPayload(ctx, &PacketReader{Reader: clientReader}, &PacketWriter{Writer: conn}, dispatcher)
}
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
From: conn.RemoteAddr(),
To: destination,
Status: log.AccessAccepted,
Reason: "",
Email: user.Email,
})
newError("received request for ", destination).WriteToLog(sid)
return s.handleConnection(ctx, sessionPolicy, destination, clientReader, buf.NewWriter(conn), dispatcher)View on GitHub (pinned to db12914161)
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.
Example fix
// before (test / custom accept loop)
func handle(conn net.Conn) {
ctx := context.Background()
go trojanServer.Process(ctx, link, conn) // panics: no inbound metadata
}
// after
func handle(conn net.Conn) {
ctx := context.Background()
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Tag: "my-trojan-inbound",
// normally filled by proxyman: Source/Gateway/Receiver as available
})
go trojanServer.Process(ctx, link, conn)
} Defensive patterns
Strategy: validation
Validate before calling
// before driving a trojan inbound handler, verify the pipeline context:
if session.InboundFromContext(ctx) == nil {
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Tag: "my-inbound",
Source: net.DestinationFromAddr(conn.RemoteAddr()),
})
}
err := trojanInbound.Process(ctx, link, conn) Type guard
func hasInboundMetadata(ctx context.Context) bool {
return session.InboundFromContext(ctx) != nil
} Try / catch
// last-resort containment around a nonstandard invocation:
func safeProcess(ctx context.Context, p proxy.Inbound, r io.Reader, w io.Writer) (err error) {
defer func() {
if r := recover(); r != nil {
err = newError("inbound handler panicked: ", r)
}
}()
return p.Process(ctx, r, w)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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).
Related errors
AI-assisted analysis of v2fly/v2ray-core@db12914161 (2026-08-15).
Data as JSON: /api/errors/83cb1f6a14c5d1fc.
Report an issue: GitHub.