How to Debug Hysteria Core Module Errors: A Complete Guide to Typed Errors

You can debug Hysteria core module errors by inspecting the six typed error types defined in core/errors/errors.go, using Go's errors.As to distinguish them at runtime, enabling verbose logging with --verbose, and correlating client failures with server-side rejection messages.

The Hysteria proxy protocol (apernet/hysteria) exposes a well-defined set of typed errors in its core library that describe exactly why client operations fail. Understanding how these errors propagate from core/client/client.go through to your application logs allows you to rapidly diagnose configuration mismatches, authentication failures, and protocol violations without guesswork.

Understanding Hysteria Core Error Types

All core module errors are defined centrally in core/errors/errors.go. Each type encapsulates a specific failure mode in the client-server communication lifecycle.

ConfigError

ConfigError indicates invalid configuration fields, such as a missing UDP flag or malformed TLS settings. The error message follows the format "invalid config: <field>: <reason>". This error is typically thrown before any network activity occurs, allowing you to catch setup issues early.

ConnectError

ConnectError wraps low-level network failures including TCP/UDP socket creation failures, TLS handshake failures, and DNS resolution errors. The message format is "connect error: <underlying error>". In core/client/client.go (lines 130-138), this error type captures any failure returned from the underlying transport creation.

AuthError

AuthError triggers when the server returns an unexpected HTTP status code during the initial authentication request. The message reads "authentication error, HTTP status code: <code>". According to the source in core/client/client.go (line 136), this occurs specifically when the HTTP response status is not 200.

DialError

DialError represents server-side rejection of a client's dial request, commonly caused by disabled UDP support or exceeded quotas. The message format is "dial error: <message>". The client decodes this from JSON responses at lines 213, 225, and 269 in core/client/client.go. For example, a message of "UDP not enabled" originates from line 225 when the server's configuration disables UDP.

ClosedError

ClosedError occurs when the client attempts to read from or write to a connection that has already been closed. Messages appear as "connection closed" or "connection closed: <underlying error>". This is returned at line 251 in core/client/client.go when operations are attempted on a closed net.Conn.

ProtocolError

ProtocolError signals malformed or unexpected protocol frames received by either peer. The format is "protocol error: <message>". When debugging this error, compare observed packet layouts against the protocol definitions in core/internal/protocol/http.go and the padding logic in core/internal/protocol/padding.go.

Where Core Errors Originate in the Source Code

Client-Side Error Generation

The bulk of error creation lives in core/client/client.go. The ConnectError wraps transport creation failures, while AuthError is produced when the HTTP response status is not 200 (line 136). DialError is emitted after the server replies to the dial request, with the message extracted directly from the JSON response (lines 213, 225, 269). ClosedError guards against operations on closed connections at line 251.

Server-Side Error Propagation

When the server decides to reject a dial request, it sends a structured JSON payload that the client decodes into a DialError. The relevant logic resides in core/server/server.go, particularly around the UDP-handshake handling. While the server does not expose a separate Go error type for these rejections, matching timestamps between client DialError logs and server warning logs reveals the exact mismatch (e.g., client expects UDP but the server's udp field is false).

Integration Test References

The integration tests in core/internal/integration_tests/smoke_test.go deliberately provoke each error type. For example, line 87 checks for DialError when UDP is disabled. Running these tests provides reproducible error scenarios that mirror production failures.

Step-by-Step Debugging Workflow

  1. Enable verbose logging. Most commands (e.g., hysteria server or hysteria client) accept the -v or --verbose flag. The logger prints the raw error string when a request fails, revealing the error type prefix.

  2. Inspect the error type programmatically. Use Go's errors.As to discover which concrete error you have:

    var dErr coreErrs.DialError
    if errors.As(err, &dErr) {
        fmt.Println("Dial failed:", dErr.Message)
    }

    This pattern is implemented in the speed-test command at app/cmd/speedtest.go (line 112).

  3. Check the originating code. Once you know the type, jump to the specific line numbers in core/client/client.go to see which conditions produce that error. For example, a DialError with the message "UDP not enabled" comes from line 225, indicating the server's configuration disables UDP.

  4. Correlate with server logs. The server writes corresponding warnings when it rejects dial requests. Matching timestamps between client and server logs usually reveals the exact configuration mismatch.

  5. Validate configuration early. Many ConfigErrors are thrown before network activity. Run hysteria client -c <conf> with --check where available, or call client.ConfigValidate() in a small Go program to identify problematic fields.

  6. Use integration tests as a sandbox. The tests in core/internal/integration_tests/ deliberately create error conditions. Running go test ./core/internal/integration_tests -run TestClientServerDialError reproduces the exact path you're investigating, allowing you to step through with a debugger.

  7. Fallback to network tracing. If the error type is ProtocolError or ConnectError and the message is vague, capture raw TCP/UDP traffic with tcpdump or Wireshark. Compare packets against the protocol definitions in core/internal/protocol/http.go.

Practical Code Examples

Distinguishing Error Types in Go

The following snippet demonstrates how to handle each core error type in a client application, matching the pattern used in app/cmd/speedtest.go:

package main

import (
	"errors"
	"fmt"
	"log"

	coreErrs "github.com/apernet/hysteria/core/v2/errors"
	"github.com/apernet/hysteria/core/v2/client"
)

func main() {
	c, err := client.NewClient("client.yaml")
	if err != nil {
		log.Fatalf("failed to load client config: %v", err)
	}
	
	// Attempt to open a UDP tunnel
	_, err = c.DialUDP()
	if err != nil {
		switch {
		case errors.As(err, &coreErrs.ConnectError{}):
			fmt.Println("Network connection failed:", err)
		case errors.As(err, &coreErrs.AuthError{}):
			fmt.Println("Authentication rejected:", err)
		case errors.As(err, &coreErrs.DialError{}):
			fmt.Println("Server rejected dial request:", err)
		case errors.As(err, &coreErrs.ClosedError{}):
			fmt.Println("Connection already closed:", err)
		default:
			fmt.Println("Unknown error:", err)
		}
	}
}

Reproducing Errors with Integration Tests

To reproduce a DialError condition in a controlled environment:

go test ./core/internal/integration_tests -run TestClientServerDialError -v

Inspect core/internal/integration_tests/smoke_test.go (line 87) to see how the test starts a server without UDP support, causing the client to receive a DialError with the message "UDP not enabled".

Enabling Verbose Logging

Run the client with verbose output to see raw error strings:

hysteria client -c client.yaml --verbose

Typical log output from app/cmd/client.go appears as:


2026-05-13T12:01:23Z    INFO    connect error: dial tcp 203.0.113.5:443: i/o timeout

The connect error prefix identifies this as a ConnectError, directing you to investigate network connectivity or TLS configuration.

Summary

  • All core errors are defined centrally in core/errors/errors.go as distinct Go types.
  • The client implementation in core/client/client.go generates errors at specific lines: ConnectError (lines 130-138), AuthError (line 136), DialError (lines 213, 225, 269), and ClosedError (line 251).
  • The client converts server-sent JSON error payloads into DialError instances.
  • Integration tests in core/internal/integration_tests/smoke_test.go provide reproducible error scenarios for debugging.
  • Use errors.As for type assertion and the --verbose flag for immediate error string visibility.

Frequently Asked Questions

How do I identify which Hysteria error type I'm dealing with?

Use Go's errors.As function to perform type assertion against the error types defined in core/errors/errors.go. This approach is used in app/cmd/speedtest.go (line 112) to handle DialError specifically. Alternatively, enable --verbose logging and inspect the error message prefix (e.g., "connect error:", "dial error:") to determine the type manually.

What causes a DialError in Hysteria?

A DialError occurs when the server actively rejects a client's dial request. Common causes include the server having UDP disabled while the client requests it, exceeding bandwidth quotas, or invalid destination addresses. The specific reason appears in the error message extracted from the server's JSON response at lines 213, 225, and 269 of core/client/client.go.

How can I reproduce Hysteria errors for debugging?

Run the integration tests in core/internal/integration_tests/ using go test. These tests deliberately trigger specific error conditions; for example, TestClientServerDialError creates a server without UDP support to force a DialError. You can set breakpoints in these tests to step through the exact error generation path in core/client/client.go.

Where are Hysteria protocol errors defined?

ProtocolError is defined alongside other core errors in core/errors/errors.go. It triggers when either peer receives malformed or unexpected protocol frames. To debug these, examine the protocol implementation in core/internal/protocol/http.go and the padding logic in core/internal/protocol/padding.go, comparing against packet captures from tcpdump or Wireshark.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →