Skip to content

httperr Package ​

httperr contains HTTP status codes and error messages to send to clients. When a controller returns an error such as httperr.NotFound("사용자를 찾을 수 없습니다"), Spine converts it to a status code and a JSON error response. Controllers do not need to work directly with response objects from Echo or similar frameworks.

This page describes github.com/NARUBROWN/spine/pkg/httperr in Spine v0.5.1.

Using It in a CLI Project ​

The controller below is a code snippet to add to a CLI-generated project. httpx, httperr, and path are packages under github.com/NARUBROWN/spine/pkg. Connect the controller constructor and route to the registration path used by BuildApplication. The app defines domain types such as User, repositories, and services.

Wrap successful values in httpx.Response[T]. On failure, return an empty response value together with the error. In examples that receive a request body, declare the DTO as a pointer, such as *PaymentRequest.

go
func (c *UserController) GetUser(userId path.Int) (httpx.Response[User], error) {
    if userId.Value <= 0 {
        return httpx.Response[User]{}, httperr.BadRequest("유효하지 않은 사용자 ID입니다")
    }

    user, err := c.repo.FindByID(userId.Value)
    if err != nil {
        return httpx.Response[User]{}, httperr.NotFound("사용자를 찾을 수 없습니다")
    }

    return httpx.Response[User]{Body: user}, nil
}

If this method is registered for GET /users/:id, it responds as follows. The example assumes that every repository lookup error is treated as a 404. In a real app, distinguish a missing result from a database failure when choosing the status code.

bash
# 잘못된 ID
GET /users/-1
→ 400 {"message": "유효하지 않은 사용자 ID입니다"}

# 존재하지 않는 사용자
GET /users/999
→ 404 {"message": "사용자를 찾을 수 없습니다"}

# 정상
GET /users/123
→ 200 {"id": 123, "name": "john"}

HTTPError Fields ​

HTTPError implements Go's error interface, and Error() returns Message.

go
// pkg/httperr/types.go
type HTTPError struct {
    Status  int    // HTTP 상태 코드
    Message string // 에러 메시지
    Cause   error  // 원인 에러 (선택)
}

// error 인터페이스 구현
func (e *HTTPError) Error() string {
    return e.Message
}
FieldTypePurpose
StatusintHTTP error status code to send to the client
MessagestringMessage to send in the JSON response's message field
CauseerrorUnderlying error retained by the app. Not included in the JSON response

The helper functions return the error interface type, but the concrete value is a *httperr.HTTPError. Use errors.As to find this type. Even when an error is wrapped with fmt.Errorf("문맥: %w", err), Spine finds the inner HTTPError and uses its status code and Message.

Message is sent to the client as is. Keep information that must not be public, such as SQL, secrets, or internal paths, in the underlying error or logs rather than in the message.

Built-in Helper Functions ​

Spine v0.5.1 provides the following four helpers. Each accepts msg string and returns error.

FunctionStatus CodeWhen to Use It
BadRequest(msg)400A request value does not meet the API's requirements
Unauthorized(msg)401Authentication information is missing or authentication fails
NotFound(msg)404The requested resource cannot be found
InternalServerError(msg)500An internal server error must be returned with an explicit message

Invalid Request Values ​

Use BadRequest to check business conditions that still need validation after type conversion.

go
func BadRequest(msg string) error {
    return &HTTPError{Status: 400, Message: msg}
}
go
if userId.Value <= 0 {
    return httpx.Response[User]{}, httperr.BadRequest("유효하지 않은 사용자 ID입니다")
}

Authentication Failure ​

Use Unauthorized when authentication is required or fails. If an already authenticated user lacks permission, you can choose a 403 status code as shown in the extension examples below.

go
func Unauthorized(msg string) error {
    return &HTTPError{Status: 401, Message: msg}
}
go
if !c.auth.IsValid(token) {
    return httpx.Response[User]{}, httperr.Unauthorized("인증이 필요합니다")
}

Missing Resources ​

Use NotFound when the requested resource does not exist.

go
func NotFound(msg string) error {
    return &HTTPError{Status: 404, Message: msg}
}
go
user, err := c.repo.FindByID(id)
if err != nil {
    return httpx.Response[User]{}, httperr.NotFound("사용자를 찾을 수 없습니다")
}

Internal Server Errors ​

InternalServerError returns status code 500 and the specified message. Unlike returning a regular error directly, this exposes the message to the client.

go
func InternalServerError(msg string) error {
    return &HTTPError{Status: 500, Message: msg}
}
go
result, err := c.externalService.Call()
if err != nil {
    return httpx.Response[Result]{}, httperr.InternalServerError("외부 서비스 호출에 실패했습니다")
}

Controller Return Values and Error Responses ​

Spine first checks the controller's return values for a non-nil error. If an error is present, it does not process the success value as a response.

Return ValuesDefault HTTP Handling
httpx.Response[T], nil errorProcesses the success value
httpx.Response[T], httperr.NotFound(...)Returns a 404 error response instead of the success value
httpx.Response[T], regular errorReturns a 500 error response instead of the success value
Only error, with a nil valueDoes not write an error response

Return JSON success values inside httpx.Response[T]. Use httpx.Redirect for redirects and httpx.Binary for binary responses. Returning an arbitrary DTO differs from returning an HTTP success response type.

Error response bodies use the {"message":"..."} format. The default handling differs as follows.

Error TypeStatus Codemessage
*httperr.HTTPErrorError status code specified by StatusMessage
Error wrapping an HTTPError with %wStatus code of the inner HTTPErrorMessage of the inner HTTPError
Regular error500"Internal server error"

The default error response does not expose the Error() string of a regular error. Use httperr to state failure conditions the client needs to know, and inspect logs for detailed causes.

Validating Business Rules ​

Even after receiving a request body, you must check app-defined conditions such as amounts, limits, and balances. Returning an error from each failure branch causes that error to be handled without creating a success response.

go
func (c *PaymentController) Process(req *PaymentRequest) (httpx.Response[Receipt], error) {
    if req.Amount <= 0 {
        return httpx.Response[Receipt]{}, httperr.BadRequest("결제 금액은 0보다 커야 합니다")
    }

    if req.Amount > 10000000 {
        return httpx.Response[Receipt]{}, httperr.BadRequest("1회 결제 한도를 초과했습니다")
    }

    balance, err := c.wallet.GetBalance(req.UserID)
    if err != nil {
        return httpx.Response[Receipt]{}, httperr.NotFound("지갑을 찾을 수 없습니다")
    }

    if balance < req.Amount {
        return httpx.Response[Receipt]{}, httperr.BadRequest("잔액이 부족합니다")
    }

    receipt, err := c.processPayment(req)
    if err != nil {
        return httpx.Response[Receipt]{}, err
    }
    return httpx.Response[Receipt]{Body: receipt}, nil
}

The app implements PaymentRequest, Receipt, wallet lookup, and payment processing in this example. The app is also responsible for distinguishing a missing wallet from a lookup system failure. If c.processPayment returns a regular error, the client receives the default 500 message.

Handlers That Return Only an Error ​

Methods that do not need a success response body can return only error.

go
func (c *UserController) DeleteUser(userId path.Int) error {
    exists, err := c.repo.Exists(userId.Value)
    if err != nil || !exists {
        return httperr.NotFound("사용자를 찾을 수 없습니다")
    }

    if err := c.repo.Delete(userId.Value); err != nil {
        return httperr.BadRequest("삭제할 수 없습니다")
    }

    return nil  // 성공 시 nil 반환
}

Returning nil does not write an error response. This form alone does not specify a particular success status code. Use an httpx response type if you need an explicit status code, such as 204 after a successful deletion.

For brevity, the example treats existence-check errors as 404 and deletion errors as 400. In a real app, choose status codes and retry policies according to the actual repository error type.

Using Other Error Status Codes ​

For status codes without a built-in helper, create an HTTPError directly. The following functions are examples defined by the app; they are not provided by httperr.

go
// 직접 HTTPError 생성
func Forbidden(msg string) error {
    return &httperr.HTTPError{Status: 403, Message: msg}
}

func Conflict(msg string) error {
    return &httperr.HTTPError{Status: 409, Message: msg}
}

func UnprocessableEntity(msg string) error {
    return &httperr.HTTPError{Status: 422, Message: msg}
}

func TooManyRequests(msg string) error {
    return &httperr.HTTPError{Status: 429, Message: msg}
}

Use error status codes from 400 through 599. Status code validity is also checked when the response is written, so do not use a success or redirect status in HTTPError.Status to control the flow.

Retaining and Logging Underlying Errors ​

Cause lets you retain both the client-facing message and the internal cause. The following WithCause helper is defined by the app.

go
func WithCause(status int, msg string, cause error) error {
    return &httperr.HTTPError{
        Status:  status,
        Message: msg,
        Cause:   cause,
    }
}

// 사용
user, err := c.repo.FindByID(id)
if err != nil {
    return httpx.Response[User]{}, WithCause(404, "사용자를 찾을 수 없습니다", err)
}

HTTPError has no Unwrap() method. An error stored in Cause is not automatically linked into Go's error chain. To inspect the cause, use errors.As to find the HTTPError, then read its Cause.

An interceptor's AfterCompletion can log the cause after the request finishes. The following example shows only that method and uses the core, httperr, errors, and log packages. A registrable implementation also needs PreHandle, PostHandle, and BeforeResponse.

go
func (i *LoggingInterceptor) AfterCompletion(ctx core.ExecutionContext, meta core.HandlerMeta, err error) {
    if err != nil {
        var httpErr *httperr.HTTPError
        if errors.As(err, &httpErr) && httpErr.Cause != nil {
            log.Printf("[ERR] %s %s: %s (cause: %v)",
                ctx.Method(), ctx.Path(), httpErr.Message, httpErr.Cause)
        }
    }
}

Runtime Errors and After Response Completion ​

Errors can occur during routing, argument conversion, controller invocation, or response processing, in addition to errors returned by a controller. Spine converts these errors into the final error response within the HTTP execution flow.

SituationDefault Behavior
No matching route404 error response
HTTPError occurs during executionResponds with the specified error status code and message
Regular execution error occursResponds with 500 and "Internal server error"
Request body exceeds the size limitResponds with 413 and "Request entity too large"
An actual response has already been writtenDoes not write another error response, preventing a double response

Spine v0.5.1 applies the prepared response to the actual transport layer after pre-response processing is complete. An error in BeforeResponse can replace the pending success response with an error response. Since AfterCompletion runs after response processing, use this stage to log errors and transmission results rather than attempting to write a new error response.

Pass errors through Go return values. Do not use panic for normal input validation or business failures. See the Interceptor Tutorial for interceptor termination and cleanup behavior.