Skip to content

Commit 16b9159

Browse files
committed
fix: UTXO endpoint to return 404 for not found
1 parent dee8ed1 commit 16b9159

4 files changed

Lines changed: 63 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Fixed UTXO endpoint to return HTTP 404 (Not Found) instead of 500 (Internal Server Error) when UTXO is not found
12+
- Added proper error type handling for API errors:
13+
- `NotFoundError` returns HTTP 404
14+
- `BadRequestError` returns HTTP 400
15+
- Invalid address or txid parameters now return 400 instead of 500
16+
1017
## [0.6.0] - 2026-01-08
1118

1219
### Removed

neutrino_server/internal/api/handler.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"encoding/hex"
99
"encoding/json"
10+
"errors"
1011
"net/http"
1112
"strconv"
1213

@@ -239,7 +240,17 @@ func (h *Handler) handleGetUTXO(w http.ResponseWriter, r *http.Request) {
239240

240241
report, err := h.node.GetUTXO(txid, uint32(vout), address, startHeight)
241242
if err != nil {
242-
h.errorResponse(w, http.StatusInternalServerError, err.Error())
243+
// Check for typed errors to return appropriate status codes
244+
var notFoundErr *neutrino.NotFoundError
245+
var badRequestErr *neutrino.BadRequestError
246+
247+
if errors.As(err, &notFoundErr) {
248+
h.errorResponse(w, http.StatusNotFound, err.Error())
249+
} else if errors.As(err, &badRequestErr) {
250+
h.errorResponse(w, http.StatusBadRequest, err.Error())
251+
} else {
252+
h.errorResponse(w, http.StatusInternalServerError, err.Error())
253+
}
243254
return
244255
}
245256

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package neutrino
2+
3+
import "fmt"
4+
5+
// NotFoundError represents an error when a requested resource is not found.
6+
// This should result in HTTP 404 responses.
7+
type NotFoundError struct {
8+
Resource string
9+
Message string
10+
}
11+
12+
func (e *NotFoundError) Error() string {
13+
if e.Message != "" {
14+
return e.Message
15+
}
16+
return fmt.Sprintf("%s not found", e.Resource)
17+
}
18+
19+
// NewNotFoundError creates a new NotFoundError.
20+
func NewNotFoundError(resource string, message string) *NotFoundError {
21+
return &NotFoundError{
22+
Resource: resource,
23+
Message: message,
24+
}
25+
}
26+
27+
// BadRequestError represents an error due to invalid client input.
28+
// This should result in HTTP 400 responses.
29+
type BadRequestError struct {
30+
Message string
31+
}
32+
33+
func (e *BadRequestError) Error() string {
34+
return e.Message
35+
}
36+
37+
// NewBadRequestError creates a new BadRequestError.
38+
func NewBadRequestError(message string) *BadRequestError {
39+
return &BadRequestError{Message: message}
40+
}

neutrino_server/internal/neutrino/node.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,13 @@ func (n *Node) GetUTXO(txid string, vout uint32, address string, startHeight int
397397
}
398398

399399
if address == "" {
400-
return nil, errors.New("address is required: neutrino uses compact block filters which match on scripts, not outpoints")
400+
return nil, NewBadRequestError("address is required: neutrino uses compact block filters which match on scripts, not outpoints")
401401
}
402402

403403
// Parse the address to get the pkScript
404404
addr, err := btcutil.DecodeAddress(address, n.chainParams)
405405
if err != nil {
406-
return nil, fmt.Errorf("invalid address %s: %w", address, err)
406+
return nil, NewBadRequestError(fmt.Sprintf("invalid address %s: %v", address, err))
407407
}
408408

409409
pkScript, err := txscript.PayToAddrScript(addr)
@@ -414,7 +414,7 @@ func (n *Node) GetUTXO(txid string, vout uint32, address string, startHeight int
414414
// Parse txid
415415
targetHash, err := chainhash.NewHashFromStr(txid)
416416
if err != nil {
417-
return nil, fmt.Errorf("invalid txid: %w", err)
417+
return nil, NewBadRequestError(fmt.Sprintf("invalid txid: %v", err))
418418
}
419419

420420
n.logger.Infof("Looking up UTXO %s:%d for address %s starting from height %d", txid, vout, address, startHeight)
@@ -513,7 +513,7 @@ func (n *Node) GetUTXO(txid string, vout uint32, address string, startHeight int
513513

514514
// Build response
515515
if foundTx == nil {
516-
return nil, fmt.Errorf("UTXO not found: ensure start_height is at or before the block containing the transaction")
516+
return nil, NewNotFoundError("UTXO", "UTXO not found: ensure start_height is at or before the block containing the transaction")
517517
}
518518

519519
report := &UTXOSpendReport{}

0 commit comments

Comments
 (0)