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

> Debug Hysteria core module errors effectively. Learn to use typed errors, errorsAs, verbose logging, and server rejection messages for quick resolution with this comprehensive guide.

- Repository: [Aperture Internet Laboratory/hysteria](https://github.com/apernet/hysteria)
- Tags: how-to-guide
- Published: 2026-05-13

---

**You can debug Hysteria core module errors by inspecting the six typed error types defined in [`core/errors/errors.go`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/core/internal/protocol/http.go) and the padding logic in [`core/internal/protocol/padding.go`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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:

   ```go
   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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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 `ConfigError`s 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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/app/cmd/speedtest.go):

```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:

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

```

Inspect [`core/internal/integration_tests/smoke_test.go`](https://github.com/apernet/hysteria/blob/main/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:

```bash
hysteria client -c client.yaml --verbose

```

Typical log output from [`app/cmd/client.go`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/core/errors/errors.go) as distinct Go types.
- The client implementation in [`core/client/client.go`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/core/errors/errors.go). This approach is used in [`app/cmd/speedtest.go`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/core/client/client.go).

### Where are Hysteria protocol errors defined?

`ProtocolError` is defined alongside other core errors in [`core/errors/errors.go`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/core/internal/protocol/http.go) and the padding logic in [`core/internal/protocol/padding.go`](https://github.com/apernet/hysteria/blob/main/core/internal/protocol/padding.go), comparing against packet captures from `tcpdump` or Wireshark.