Unify fallback and unimplemented API error handling

- Add a shared common error response schema and unknown error code for business-level fallbacks
- Return business error payloads for router NoRoute and NoMethod fallbacks
- Align the existing event list fallback response with the shared error structure
- Introduce an explicit unimplemented API error type instead of relying on formatted strings
- Make /api batch dispatch return per-item status 600 error entries for unimplemented modules and actions without failing the whole batch

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-06 17:13:27 +08:00
parent f6466989c6
commit 0dd5df0302
35 changed files with 149 additions and 72 deletions
+2
View File
@@ -4,6 +4,8 @@ type ErrorCode int
// https://github.com/DarkEnergyProcessor/NPPS4/blob/master/npps4/idol/error.py // https://github.com/DarkEnergyProcessor/NPPS4/blob/master/npps4/idol/error.py
const ( const (
ErrorCodeUnknown ErrorCode = 1
ErrorCodeNoUnitIsSellable ErrorCode = 1205 ErrorCodeNoUnitIsSellable ErrorCode = 1205
ErrorCodeLivePreciseListNotFound ErrorCode = 3421 ErrorCodeLivePreciseListNotFound ErrorCode = 3421
ErrorCodeEventNoEventData ErrorCode = 10004
) )
+2 -2
View File
@@ -1,7 +1,7 @@
package album package album
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func AlbumApi(ctx *gin.Context, action string) (res any, err error) {
case "albumAll": case "albumAll":
res, err = albumAll(ctx) res, err = albumAll(ctx)
default: default:
err = fmt.Errorf("unimplemented action: album: %s", action) err = honokautils.NewUnimplementedActionError("album", action)
} }
return res, err return res, err
} }
+15 -2
View File
@@ -1,7 +1,7 @@
package api package api
import ( import (
"fmt" "honoka-chan/internal/constant"
"honoka-chan/internal/handler/api/album" "honoka-chan/internal/handler/api/album"
"honoka-chan/internal/handler/api/award" "honoka-chan/internal/handler/api/award"
"honoka-chan/internal/handler/api/background" "honoka-chan/internal/handler/api/background"
@@ -32,8 +32,10 @@ import (
"honoka-chan/internal/middleware" "honoka-chan/internal/middleware"
"honoka-chan/internal/router" "honoka-chan/internal/router"
apischema "honoka-chan/internal/schema/api" apischema "honoka-chan/internal/schema/api"
commonschema "honoka-chan/internal/schema/common"
"honoka-chan/internal/session" "honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils" honokautils "honoka-chan/internal/utils"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -108,9 +110,20 @@ func api(ctx *gin.Context) {
case "user": case "user":
result, err = user.UserApi(ctx, v.Action) result, err = user.UserApi(ctx, v.Action)
default: default:
err = fmt.Errorf("unimplemented api module: %s", v.Module) err = honokautils.NewUnimplementedModuleError(v.Module)
} }
if honokautils.IsUnimplementedError(err) {
results = append(results, commonschema.ApiErrorResp{
Result: commonschema.ErrorData{
ErrorCode: constant.ErrorCodeUnknown,
},
Status: 600,
CommandNum: false,
TimeStamp: time.Now().Unix(),
})
continue
}
if ss.CheckErr(err) { if ss.CheckErr(err) {
return return
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package award package award
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func AwardApi(ctx *gin.Context, action string) (res any, err error) {
case "awardInfo": case "awardInfo":
res, err = awardInfo(ctx) res, err = awardInfo(ctx)
default: default:
err = fmt.Errorf("unimplemented action: award: %s", action) err = honokautils.NewUnimplementedActionError("award", action)
} }
return res, err return res, err
} }
@@ -1,7 +1,7 @@
package background package background
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func BackgroundApi(ctx *gin.Context, action string) (res any, err error) {
case "backgroundInfo": case "backgroundInfo":
res, err = backgroundInfo(ctx) res, err = backgroundInfo(ctx)
default: default:
err = fmt.Errorf("unimplemented action: background: %s", action) err = honokautils.NewUnimplementedActionError("background", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package banner package banner
import "fmt" import honokautils "honoka-chan/internal/utils"
func BannerApi(action string) (res any, err error) { func BannerApi(action string) (res any, err error) {
switch action { switch action {
case "bannerList": case "bannerList":
res, err = bannerList() res, err = bannerList()
default: default:
err = fmt.Errorf("unimplemented action: banner: %s", action) err = honokautils.NewUnimplementedActionError("banner", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package challenge package challenge
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
) )
func ChallengeApi(action string) (res any, err error) { func ChallengeApi(action string) (res any, err error) {
@@ -9,7 +9,7 @@ func ChallengeApi(action string) (res any, err error) {
case "challengeInfo": case "challengeInfo":
res, err = challengeInfo() res, err = challengeInfo()
default: default:
err = fmt.Errorf("unimplemented action: challenge: %s", action) err = honokautils.NewUnimplementedActionError("challenge", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package costume package costume
import "fmt" import honokautils "honoka-chan/internal/utils"
func CostumeApi(action string) (res any, err error) { func CostumeApi(action string) (res any, err error) {
switch action { switch action {
case "costumeList": case "costumeList":
res, err = costumeList() res, err = costumeList()
default: default:
err = fmt.Errorf("unimplemented action: costume: %s", action) err = honokautils.NewUnimplementedActionError("costume", action)
} }
return res, err return res, err
} }
@@ -1,7 +1,7 @@
package eventscenario package eventscenario
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func EventScenarioApi(ctx *gin.Context, action string) (res any, err error) {
case "status": case "status":
res, err = eventScenarioStatus(ctx) res, err = eventScenarioStatus(ctx)
default: default:
err = fmt.Errorf("unimplemented action: costume: %s", action) err = honokautils.NewUnimplementedActionError("eventscenario", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package exchange package exchange
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func ExchangeApi(ctx *gin.Context, action string) (res any, err error) {
case "owningPoint": case "owningPoint":
res, err = owningPoint(ctx) res, err = owningPoint(ctx)
default: default:
err = fmt.Errorf("unimplemented action: exchange: %s", action) err = honokautils.NewUnimplementedActionError("exchange", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package item package item
import "fmt" import honokautils "honoka-chan/internal/utils"
func ItemApi(action string) (res any, err error) { func ItemApi(action string) (res any, err error) {
switch action { switch action {
case "list": case "list":
res, err = itemList() res, err = itemList()
default: default:
err = fmt.Errorf("unimplemented action: item: %s", action) err = honokautils.NewUnimplementedActionError("item", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package live package live
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -13,7 +13,7 @@ func LiveApi(ctx *gin.Context, action string) (res any, err error) {
case "schedule": case "schedule":
res, err = liveSchedule(ctx) res, err = liveSchedule(ctx)
default: default:
err = fmt.Errorf("unimplemented action: live: %s", action) err = honokautils.NewUnimplementedActionError("live", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package liveicon package liveicon
import "fmt" import honokautils "honoka-chan/internal/utils"
func LiveIconApi(action string) (res any, err error) { func LiveIconApi(action string) (res any, err error) {
switch action { switch action {
case "liveiconInfo": case "liveiconInfo":
res, err = liveIconInfo() res, err = liveIconInfo()
default: default:
err = fmt.Errorf("unimplemented action: liveicon: %s", action) err = honokautils.NewUnimplementedActionError("liveicon", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package livese package livese
import "fmt" import honokautils "honoka-chan/internal/utils"
func LiveSeApi(action string) (res any, err error) { func LiveSeApi(action string) (res any, err error) {
switch action { switch action {
case "liveseInfo": case "liveseInfo":
res, err = LiveSeInfo() res, err = LiveSeInfo()
default: default:
err = fmt.Errorf("unimplemented action: livese: %s", action) err = honokautils.NewUnimplementedActionError("livese", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package login package login
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -13,7 +13,7 @@ func LoginApi(ctx *gin.Context, action string) (res any, err error) {
case "topInfoOnce": case "topInfoOnce":
res, err = loginTopInfoOnce(ctx) res, err = loginTopInfoOnce(ctx)
default: default:
err = fmt.Errorf("unimplemented action: login: %s", action) err = honokautils.NewUnimplementedActionError("login", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package marathon package marathon
import "fmt" import honokautils "honoka-chan/internal/utils"
func MarathonApi(action string) (res any, err error) { func MarathonApi(action string) (res any, err error) {
switch action { switch action {
case "marathonInfo": case "marathonInfo":
res, err = marathonInfo() res, err = marathonInfo()
default: default:
err = fmt.Errorf("unimplemented action: marathon: %s", action) err = honokautils.NewUnimplementedActionError("marathon", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package multiunit package multiunit
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func MultiUnitApi(ctx *gin.Context, action string) (res any, err error) {
case "multiunitscenarioStatus": case "multiunitscenarioStatus":
res, err = MultiUnitScenarioStatus(ctx) res, err = MultiUnitScenarioStatus(ctx)
default: default:
err = fmt.Errorf("unimplemented action: multiunit: %s", action) err = honokautils.NewUnimplementedActionError("multiunit", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package museum package museum
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func MuseumApi(ctx *gin.Context, action string) (res any, err error) {
case "info": case "info":
res, err = museumInfo(ctx) res, err = museumInfo(ctx)
default: default:
err = fmt.Errorf("unimplemented action: museum: %s", action) err = honokautils.NewUnimplementedActionError("museum", action)
} }
return res, err return res, err
} }
@@ -1,7 +1,7 @@
package navigation package navigation
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
) )
func NavigationApi(action string) (res any, err error) { func NavigationApi(action string) (res any, err error) {
@@ -9,7 +9,7 @@ func NavigationApi(action string) (res any, err error) {
case "specialCutin": case "specialCutin":
res, err = SpecialCutin() res, err = SpecialCutin()
default: default:
err = fmt.Errorf("unimplemented action: navigation: %s", action) err = honokautils.NewUnimplementedActionError("navigation", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package notice package notice
import "fmt" import honokautils "honoka-chan/internal/utils"
func NoticeApi(action string) (res any, err error) { func NoticeApi(action string) (res any, err error) {
switch action { switch action {
case "noticeMarquee": case "noticeMarquee":
res, err = noticeMarquee() res, err = noticeMarquee()
default: default:
err = fmt.Errorf("unimplemented action: notice: %s", action) err = honokautils.NewUnimplementedActionError("notice", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package payment package payment
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func PaymentApi(ctx *gin.Context, action string) (res any, err error) {
case "productList": case "productList":
res, err = productList(ctx) res, err = productList(ctx)
default: default:
err = fmt.Errorf("unimplemented action: payment: %s", action) err = honokautils.NewUnimplementedActionError("payment", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package profile package profile
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -15,7 +15,7 @@ func ProfileApi(ctx *gin.Context, action string, targetUserID int) (res any, err
case "profileInfo": case "profileInfo":
res, err = profileInfo(ctx, targetUserID) res, err = profileInfo(ctx, targetUserID)
default: default:
err = fmt.Errorf("unimplemented action: profile: %s", action) err = honokautils.NewUnimplementedActionError("profile", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package reward package reward
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
) )
func RewardApi(action string) (res any, err error) { func RewardApi(action string) (res any, err error) {
@@ -11,7 +11,7 @@ func RewardApi(action string) (res any, err error) {
case "rewardHistory": case "rewardHistory":
res, err = rewardHistory() res, err = rewardHistory()
default: default:
err = fmt.Errorf("unimplemented action: reward: %s", action) err = honokautils.NewUnimplementedActionError("reward", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package scenario package scenario
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func ScenarioApi(ctx *gin.Context, action string) (res any, err error) {
case "scenarioStatus": case "scenarioStatus":
res, err = scenarioStatus(ctx) res, err = scenarioStatus(ctx)
default: default:
err = fmt.Errorf("unimplemented action: scenario: %s", action) err = honokautils.NewUnimplementedActionError("scenario", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package secretbox package secretbox
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
) )
func SecretBoxApi(action string) (res any, err error) { func SecretBoxApi(action string) (res any, err error) {
@@ -9,7 +9,7 @@ func SecretBoxApi(action string) (res any, err error) {
case "all": case "all":
res, err = all() res, err = all()
default: default:
err = fmt.Errorf("unimplemented action: secretbox: %s", action) err = honokautils.NewUnimplementedActionError("secretbox", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,13 +1,13 @@
package stamp package stamp
import "fmt" import honokautils "honoka-chan/internal/utils"
func StampApi(action string) (res any, err error) { func StampApi(action string) (res any, err error) {
switch action { switch action {
case "stampInfo": case "stampInfo":
res, err = stampInfo() res, err = stampInfo()
default: default:
err = fmt.Errorf("unimplemented action: stamp: %s", action) err = honokautils.NewUnimplementedActionError("stamp", action)
} }
return res, err return res, err
} }
@@ -1,7 +1,7 @@
package subscenario package subscenario
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -11,7 +11,7 @@ func SubscenarioApi(ctx *gin.Context, action string) (res any, err error) {
case "subscenarioStatus": case "subscenarioStatus":
res, err = subscenarioStatus(ctx) res, err = subscenarioStatus(ctx)
default: default:
err = fmt.Errorf("unimplemented action: subscenario: %s", action) err = honokautils.NewUnimplementedActionError("subscenario", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package unit package unit
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -23,7 +23,7 @@ func UnitApi(ctx *gin.Context, action string) (res any, err error) {
case "unitAll": case "unitAll":
res, err = unitAll(ctx) res, err = unitAll(ctx)
default: default:
err = fmt.Errorf("unimplemented action: unit: %s", action) err = honokautils.NewUnimplementedActionError("unit", action)
} }
return res, err return res, err
} }
+2 -2
View File
@@ -1,7 +1,7 @@
package user package user
import ( import (
"fmt" honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -13,7 +13,7 @@ func UserApi(ctx *gin.Context, action string) (res any, err error) {
case "userInfo": case "userInfo":
res, err = userInfo(ctx) res, err = userInfo(ctx)
default: default:
err = fmt.Errorf("unimplemented action: user: %s", action) err = honokautils.NewUnimplementedActionError("user", action)
} }
return res, err return res, err
} }
+5 -13
View File
@@ -1,11 +1,12 @@
package event package event
import ( import (
"honoka-chan/internal/constant"
"honoka-chan/internal/middleware" "honoka-chan/internal/middleware"
"honoka-chan/internal/router" "honoka-chan/internal/router"
commonschema "honoka-chan/internal/schema/common"
eventschema "honoka-chan/internal/schema/event" eventschema "honoka-chan/internal/schema/event"
"honoka-chan/internal/session" "honoka-chan/internal/session"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -22,21 +23,12 @@ func list(ctx *gin.Context) {
}) })
} }
// ss.Respond(event.ListResp{
// ResponseData: map[string]int{
// "error_code": 10004,
// },
// ReleaseInfo: []any{},
// StatusCode: 600,
// })
ss.Respond(eventschema.ListResp{ ss.Respond(eventschema.ListResp{
ResponseData: eventschema.ListData{ ResponseData: commonschema.ErrorData{
TargetList: targets, ErrorCode: constant.ErrorCodeEventNoEventData,
ServerTimestamp: time.Now().Unix(),
}, },
ReleaseInfo: []any{}, ReleaseInfo: []any{},
StatusCode: 200, StatusCode: 600,
}) })
} }
+18
View File
@@ -2,6 +2,9 @@
package router package router
import ( import (
"honoka-chan/internal/constant"
commonschema "honoka-chan/internal/schema/common"
"honoka-chan/internal/session"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@@ -104,6 +107,21 @@ func SifRouter(r *gin.Engine) {
ctx.HTML(http.StatusOK, "common/manga.html", gin.H{}) ctx.HTML(http.StatusOK, "common/manga.html", gin.H{})
}) })
notFoundHandler := func(ctx *gin.Context) {
ss := session.Attach(ctx)
defer ss.Finalize()
ss.Respond(commonschema.ErrorResp{
ResponseData: commonschema.ErrorData{
ErrorCode: constant.ErrorCodeUnknown,
},
ReleaseInfo: []any{},
StatusCode: 600,
})
}
r.NoRoute(notFoundHandler)
r.NoMethod(notFoundHandler)
// router // router
for groupPath, groupInfo := range groups { for groupPath, groupInfo := range groups {
groupApi := r.Group(groupPath, groupInfo.InitialHandlers...) groupApi := r.Group(groupPath, groupInfo.InitialHandlers...)
+16 -1
View File
@@ -1,7 +1,22 @@
package commonschema package commonschema
import "honoka-chan/internal/constant" import (
"honoka-chan/internal/constant"
)
type ErrorData struct { type ErrorData struct {
ErrorCode constant.ErrorCode `json:"error_code"` ErrorCode constant.ErrorCode `json:"error_code"`
} }
type ErrorResp struct {
ResponseData ErrorData `json:"response_data"`
ReleaseInfo []any `json:"release_info"`
StatusCode int `json:"status_code"`
}
type ApiErrorResp struct {
Result ErrorData `json:"result"`
Status int `json:"status"`
CommandNum bool `json:"commandNum"`
TimeStamp int64 `json:"timeStamp"`
}
+37
View File
@@ -0,0 +1,37 @@
package honokautils
import "errors"
type UnimplementedError struct {
Kind string
Module string
Name string
}
func (e *UnimplementedError) Error() string {
if e == nil {
return "unimplemented"
}
return "unimplemented " + e.Kind + ": " + e.Module + ": " + e.Name
}
func NewUnimplementedModuleError(module string) error {
return &UnimplementedError{
Kind: "api module",
Module: module,
Name: module,
}
}
func NewUnimplementedActionError(module string, action string) error {
return &UnimplementedError{
Kind: "action",
Module: module,
Name: action,
}
}
func IsUnimplementedError(err error) bool {
var target *UnimplementedError
return err != nil && errors.As(err, &target)
}
+1 -1
View File
@@ -1,4 +1,4 @@
package utils package honokautils
import ( import (
"encoding/json" "encoding/json"
+1 -1
View File
@@ -1,4 +1,4 @@
package utils package honokautils
import ( import (
"encoding/json" "encoding/json"