Replace string-matching error detection with typed SQLite errors (closes #39) (#66)
All checks were successful
check / check (push) Successful in 4s

## Summary

Replaces fragile `strings.Contains(err.Error(), "UNIQUE")` checks with typed error detection using `errors.As` and the SQLite driver's `*sqlite.Error` type.

## Changes

- **`internal/db/errors.go`** (new): Adds `IsUniqueConstraintError(err)` helper that uses `errors.As` to unwrap the error into `*sqlite.Error` and checks for `SQLITE_CONSTRAINT_UNIQUE` (code 2067).
- **`internal/handlers/api.go`**: Replaces two `strings.Contains(err.Error(), "UNIQUE")` calls with `db.IsUniqueConstraintError(err)` — in `handleCreateSessionError` and `executeNickChange`.
- **`internal/handlers/auth.go`**: Replaces one `strings.Contains(err.Error(), "UNIQUE")` call with `db.IsUniqueConstraintError(err)` — in `handleRegisterError`.

## Why

String matching on error messages is fragile — if the SQLite driver changes its error message format, the detection silently breaks. Using `errors.As` with the driver's typed error and checking the specific SQLite error code is robust, idiomatic Go, and immune to message format changes.

closes #39

<!-- session: agent:sdlc-manager:subagent:3fb0b8e2-d635-4848-a5bd-131c5033cdb1 -->

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #66
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #66.
This commit is contained in:
2026-03-10 11:54:27 +01:00
committed by Jeffrey Paul
parent c07f94a432
commit b1fd2f1b96
3 changed files with 26 additions and 3 deletions

20
internal/db/errors.go Normal file
View File

@@ -0,0 +1,20 @@
// Package db provides database access and migration management.
package db
import (
"errors"
"modernc.org/sqlite"
sqlite3 "modernc.org/sqlite/lib"
)
// IsUniqueConstraintError reports whether err is a SQLite
// unique-constraint violation.
func IsUniqueConstraintError(err error) bool {
var sqliteErr *sqlite.Error
if !errors.As(err, &sqliteErr) {
return false
}
return sqliteErr.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"time"
"git.eeqj.de/sneak/neoirc/internal/db"
"git.eeqj.de/sneak/neoirc/internal/irc"
"github.com/go-chi/chi"
)
@@ -199,7 +200,7 @@ func (hdlr *Handlers) handleCreateSessionError(
request *http.Request,
err error,
) {
if strings.Contains(err.Error(), "UNIQUE") {
if db.IsUniqueConstraintError(err) {
hdlr.respondError(
writer, request,
"nick already taken",
@@ -1427,7 +1428,7 @@ func (hdlr *Handlers) executeNickChange(
request.Context(), sessionID, newNick,
)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE") {
if db.IsUniqueConstraintError(err) {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrNicknameInUse, nick, []string{newNick},

View File

@@ -4,6 +4,8 @@ import (
"encoding/json"
"net/http"
"strings"
"git.eeqj.de/sneak/neoirc/internal/db"
)
const minPasswordLength = 8
@@ -94,7 +96,7 @@ func (hdlr *Handlers) handleRegisterError(
request *http.Request,
err error,
) {
if strings.Contains(err.Error(), "UNIQUE") {
if db.IsUniqueConstraintError(err) {
hdlr.respondError(
writer, request,
"nick already taken",