refactor: use errors.Is()/errors.AsType() for error comparison and rename shadowed variables

Replace direct error equality checks (err == pgx.ErrNoRows, err != http.ErrServerClosed)
with the idiomatic errors.Is() function throughout handlers, services, middleware, and app
startup. This correctly handles wrapped error chains.

Also replace a raw type assertion (*HTTPError) with errors.AsType[*HTTPError]() in the
error handler middleware for consistency.

Additionally, rename shadowed variables for clarity:
- sidecar.go: config -> sidecarConfig, systemConfig (shadowed package-level vars)
- media_scanner.go: uuid -> uuidString (shadowed the uuid package import)
This commit is contained in:
2026-04-20 21:20:22 -04:00
parent d8d6334052
commit 9ccff320a1
15 changed files with 59 additions and 45 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ package app
import (
"context"
"errors"
"log"
"net/http"
"os"
@@ -40,7 +41,7 @@ func (a *App) StartServer(addr string) error {
// Start HTTP server in background
go func() {
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := a.server.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Server failed to start: %v", err)
}
}()
+4 -3
View File
@@ -3,6 +3,7 @@ package handlers
import (
"bookhoard/internal/database"
"context"
"errors"
"fmt"
"net/http"
"time"
@@ -98,7 +99,7 @@ func (h *AnalyticsHandler) GetReadingStats(c *echo.Context) error {
CreatedAt_2: pgtype.Timestamptz{Time: endTime, Valid: true},
})
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get reading history")
}
@@ -211,7 +212,7 @@ func (h *AnalyticsHandler) GetDeviceUsage(c *echo.Context) error {
ctx := context.Background()
usage, err := h.db.GetUserDeviceUsage(ctx, user.ID)
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get device usage")
}
@@ -263,7 +264,7 @@ func (h *AnalyticsHandler) GetPopularBooks(c *echo.Context) error {
Limit: limitInt,
})
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get popular books")
}
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"bookhoard/internal/database"
"bookhoard/internal/middleware"
"context"
"errors"
"fmt"
"net/http"
"os"
@@ -887,7 +888,7 @@ func (h *AuthHandler) DeleteUser(c *echo.Context) error {
// Delete user (this will cascade to delete all related data)
err = h.db.DeleteUser(c.Request().Context(), targetUserUUID)
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusNotFound, `<div class="text-red-500">User not found</div>`)
}
@@ -940,7 +941,7 @@ func (h *AuthHandler) UpdateUserMaxDevices(c *echo.Context) error {
MaxDevices: pgtype.Int4{Int32: req.MaxDevices, Valid: true},
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+2 -1
View File
@@ -6,6 +6,7 @@ import (
wsync "bookhoard/internal/sync"
"bookhoard/internal/utils"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
@@ -891,7 +892,7 @@ func (h *CollectionHandler) PreviewCollection(c *echo.Context) error {
_, err = h.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+7 -6
View File
@@ -5,6 +5,7 @@ import (
wsync "bookhoard/internal/sync"
"context"
"encoding/json"
"errors"
"net/http"
"time"
@@ -83,7 +84,7 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
conflicts, err = h.db.ListSyncConflictsByUser(ctx, user.ID)
}
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, 0, 0, err
}
@@ -150,7 +151,7 @@ func (h *ConflictHandler) GetConflict(c *echo.Context) error {
conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true}
conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID)
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "conflict not found")
}
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict")
@@ -214,7 +215,7 @@ func (h *ConflictHandler) ResolveConflict(c *echo.Context) error {
conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true}
conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID)
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "conflict not found")
}
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict")
@@ -292,7 +293,7 @@ func (h *ConflictHandler) applyProgressResolution(mediaItemID pgtype.UUID, userI
MediaItemID: mediaItemID,
UserID: userID,
})
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err
}
@@ -380,7 +381,7 @@ func (h *ConflictHandler) DeleteConflict(c *echo.Context) error {
conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true}
conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID)
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "conflict not found")
}
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict")
@@ -641,7 +642,7 @@ func (h *ConflictHandler) applyResolution(mediaItemID pgtype.UUID, userID pgtype
MediaItemID: mediaItemID,
UserID: userID,
})
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err
}
+7 -6
View File
@@ -6,6 +6,7 @@ import (
"bookhoard/internal/utils"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@@ -728,7 +729,7 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -836,7 +837,7 @@ func (mh *MediaHandler) GetMediaRating(c *echo.Context) error {
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, map[string]interface{}{"rating": nil})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -894,7 +895,7 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, map[string]interface{}{
"current_page": 0,
"total_pages": nil,
@@ -1026,7 +1027,7 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error {
_, err = mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1237,7 +1238,7 @@ func (mh *MediaHandler) GetMediaNote(c *echo.Context) error {
note, err := mh.db.GetMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1377,7 +1378,7 @@ func (mh *MediaHandler) GetMediaHighlight(c *echo.Context) error {
highlight, err := mh.db.GetMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+2 -1
View File
@@ -5,6 +5,7 @@ import (
wsync "bookhoard/internal/sync"
"bookhoard/internal/utils"
"context"
"errors"
"net/http"
"strconv"
"time"
@@ -43,7 +44,7 @@ func (h *Handler) GetUniversalProgress(c *echo.Context) error {
UserID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, map[string]interface{}{
"media_item_id": mediaItemID,
"progress": nil,
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"bookhoard/internal/database"
"bookhoard/internal/services"
"context"
"errors"
"fmt"
"net/http"
"os"
@@ -63,7 +64,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
// Get media item
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch media item"})
@@ -108,7 +109,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
UserID: userData.ID,
})
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
progress = database.ReadingProgress{}
}
@@ -316,7 +317,7 @@ func (h *ReaderHandler) GetReadingSpeed(c *echo.Context) error {
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
// Return zero values if no reading has occurred
return c.JSON(http.StatusOK, map[string]interface{}{
"words_per_minute": 0,
@@ -597,7 +598,7 @@ func (h *ReaderHandler) ParseEbook(c *echo.Context) error {
// Fetch media item
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(404, map[string]string{"error": "Media item not found"})
}
return c.JSON(500, map[string]string{"error": "Failed to fetch media item"})
+3 -2
View File
@@ -3,6 +3,7 @@ package handlers
import (
"bookhoard/internal/database"
"context"
"errors"
"fmt"
"net/http"
"time"
@@ -52,7 +53,7 @@ func (h *AuthHandler) RefreshAccessToken(c *echo.Context) error {
tokenInfo, err := h.db.GetRefreshToken(c.Request().Context(), tokenUUID)
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid or expired refresh token"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to validate refresh token"})
@@ -88,7 +89,7 @@ func (h *AuthHandler) Logout(c *echo.Context) error {
}
err = h.db.RevokeRefreshToken(c.Request().Context(), tokenUUID)
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to revoke refresh token"})
}
+4 -4
View File
@@ -171,7 +171,7 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
}
// Build sidecar config
config := SidecarConfig{
sidecarConfig := SidecarConfig{
Version: "1.0",
Bookhoard: SidecarBookhoardConfig{
OPDSCatalog: opdsCatalogURL,
@@ -188,7 +188,7 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
LastUpdated: time.Now().Format(time.RFC3339),
}
return c.JSON(http.StatusOK, config)
return c.JSON(http.StatusOK, sidecarConfig)
}
// DownloadSidecarConfig generates a .bookhoard.json file for device setup
@@ -352,8 +352,8 @@ func (h *SidecarHandler) GetSystemConfiguration(c *echo.Context) error {
// Build config map
result := make(map[string]string)
for _, config := range configs {
result[config.Key] = config.Value
for _, systemConfig := range configs {
result[systemConfig.Key] = systemConfig.Value
}
return c.JSON(http.StatusOK, result)
+5 -4
View File
@@ -2,6 +2,7 @@ package handlers
import (
"bookhoard/internal/database"
"errors"
"net/http"
"strconv"
@@ -47,7 +48,7 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
SettingValue: scanFrequencyValue,
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -58,7 +59,7 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
SettingValue: autoScanValue,
})
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -74,7 +75,7 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error {
scanFrequencySetting, err := h.db.GetSystemSetting(c.Request().Context(), "scan_poll_interval_seconds")
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, ScanSettingsResponse{
ScanPollIntervalSeconds: 60,
AutoScanEnabled: true,
@@ -85,7 +86,7 @@ func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error {
autoScanSetting, err := h.db.GetSystemSetting(c.Request().Context(), "auto_scan_enabled")
if err != nil {
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, ScanSettingsResponse{
ScanPollIntervalSeconds: 60,
AutoScanEnabled: true,
+2 -1
View File
@@ -1,6 +1,7 @@
package middleware
import (
"errors"
"fmt"
"net/http"
@@ -83,7 +84,7 @@ func WrapHandler(fn func(*echo.Context) error) echo.HandlerFunc {
return func(c *echo.Context) error {
err := fn(c)
if err != nil {
if httpErr, ok := err.(*HTTPError); ok {
if httpErr, ok := errors.AsType[*HTTPError](err); ok {
return RespondWithHTTPError(c, httpErr)
}
return RespondWithError(c, http.StatusInternalServerError, "Internal server error", err)
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"bookhoard/internal/services"
"bookhoard/templates"
"bytes"
"errors"
"fmt"
"net/http"
@@ -73,7 +74,7 @@ func registerReaderRoutes(cfg *Config) {
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
UserID: uuidToPGType(userUUID),
})
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
progress = database.ReadingProgress{}
}
// Get bookmarks
+9 -8
View File
@@ -15,6 +15,7 @@ import (
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"image"
_ "image/jpeg"
@@ -590,7 +591,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
fmt.Printf("Media item already exists with same size, skipping: %s\n", path)
return false, nil
}
} else if err != pgx.ErrNoRows {
} else if !errors.Is(err, pgx.ErrNoRows) {
fmt.Printf("Database error checking media item existence: %v\n", err)
return false, fmt.Errorf("failed to check if media item exists: %v", err)
}
@@ -2605,7 +2606,7 @@ func (s *MediaScanner) scanDirectory(ctx context.Context, dirPath string) {
LibraryID: libraryID,
})
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
if _, err := s.processMediaFile(ctx, path); err != nil {
s.errors++
} else {
@@ -2780,7 +2781,7 @@ func (s *MediaScanner) SyncFilesystemWithDatabase(ctx context.Context) error {
FilePath: relPath,
LibraryID: libraryID,
})
if err == pgx.ErrNoRows {
if errors.Is(err, pgx.ErrNoRows) {
// New file found - scan it
absPath := folder + "/" + relPath
if _, err := os.Stat(absPath); err == nil {
@@ -2848,19 +2849,19 @@ func (s *MediaScanner) extractOPFIdentifiers(epubPath string) (opfIdentifier, op
return "", "", "low", nil
}
var identifier, uuid string
var identifier, uuidString string
for _, id := range identifiers {
id = strings.TrimSpace(id)
// Check for UUID format (urn:uuid:)
if trimmed, found := strings.CutPrefix(strings.ToLower(id), "urn:uuid:"); found {
uuid = trimmed
uuidString = trimmed
continue
}
// Check if it's a plain UUID (8-4-4-4-12 format)
if isValidUUID(id) {
uuid = id
uuidString = id
continue
}
@@ -2879,9 +2880,9 @@ func (s *MediaScanner) extractOPFIdentifiers(epubPath string) (opfIdentifier, op
}
}
confidence = s.determineHashConfidence(uuid, identifier)
confidence = s.determineHashConfidence(uuidString, identifier)
return identifier, uuid, confidence, nil
return identifier, uuidString, confidence, nil
}
// isValidUUID checks if string is a valid UUID (8-4-4-4-12 format)
+2 -1
View File
@@ -3,6 +3,7 @@ package services
import (
"bookhoard/internal/database"
"context"
"errors"
"strings"
"github.com/jackc/pgx/v5"
@@ -214,7 +215,7 @@ func (s *SearchService) SearchFieldValues(ctx context.Context, params FieldSearc
// Shared between JSON endpoint and HTML rendering
func (s *SearchService) ExecuteSearch(ctx context.Context, params SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
results, err := s.SearchMediaItemsUnified(ctx, params)
if err != nil && err != pgx.ErrNoRows {
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, 0, err
}