feat: release managed service generation and contract diagnostics
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package compatibility_test
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"example.com/compatibility/internal/handler"
|
||||
"example.com/compatibility/internal/servicecontext"
|
||||
"example.com/compatibility/internal/svc"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
"io"
|
||||
@@ -25,7 +25,7 @@ func TestGeneratedCompatibility(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
deps, err := svc.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"example.com/compatibility/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) Echo(ctx context.Context, req *types.EchoReq) (*types.EchoResp, error) {
|
||||
func (s *Logic) Echo(ctx context.Context, req *types.EchoReq) (*types.EchoResp, error) {
|
||||
return &types.EchoResp{ID: req.ID, Data: req.Data}, nil
|
||||
}
|
||||
func (s *Service) Item(ctx context.Context, req *types.ItemReq) (*types.EchoResp, error) {
|
||||
func (s *Logic) Item(ctx context.Context, req *types.ItemReq) (*types.EchoResp, error) {
|
||||
return &types.EchoResp{ID: req.ID, Data: map[string]any{"found": true}}, nil
|
||||
}
|
||||
func (s *Service) Custom(ctx context.Context, req *types.CustomReq) (*types.CustomResp, error) {
|
||||
func (s *Logic) Custom(ctx context.Context, req *types.CustomReq) (*types.CustomResp, error) {
|
||||
return &types.CustomResp{Value: req.Value}, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package consumer_test
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"example.com/errorCatalog/internal/apierrors"
|
||||
"example.com/errorCatalog/internal/handler"
|
||||
"example.com/errorCatalog/internal/servicecontext"
|
||||
"example.com/errorCatalog/internal/svc"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
@@ -35,7 +35,7 @@ func TestGeneratedCatalog(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
deps, err := svc.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package consumer_test
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"example.com/errorCatalog/internal/handler"
|
||||
"example.com/errorCatalog/internal/servicecontext"
|
||||
"example.com/errorCatalog/internal/svc"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
@@ -31,7 +31,7 @@ func TestGoOwnedCatalogOnGeneratedRoutes(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
deps, err := svc.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ func check(kind string) (*types.CheckResponse, error) {
|
||||
return nil, apierrors.ByCode("USERNAME_INVALID")
|
||||
}
|
||||
}
|
||||
func (s *Service) Plain(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
func (s *Logic) Plain(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
return check(req.Kind)
|
||||
}
|
||||
func (s *Service) Raw(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
func (s *Logic) Raw(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
return check(req.Kind)
|
||||
}
|
||||
func (s *Service) Cleared(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
func (s *Logic) Cleared(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
return check(req.Kind)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package consumer_test
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"example.com/errorCatalog/internal/apierrors"
|
||||
"example.com/errorCatalog/internal/handler"
|
||||
"example.com/errorCatalog/internal/servicecontext"
|
||||
"example.com/errorCatalog/internal/svc"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
@@ -87,7 +87,7 @@ func TestGeneratedTemplateErrors(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
deps, err := svc.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
24
cmd/go-omni/apigen/testdata/managed/client.go
vendored
Normal file
24
cmd/go-omni/apigen/testdata/managed/client.go
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var Opened, Closed, DownloadClosed atomic.Int32
|
||||
var Started = make(chan struct{}, 1)
|
||||
var Release = make(chan struct{})
|
||||
|
||||
// This fake client exercises the same lifetime contract as a database/client.
|
||||
type Client struct{}
|
||||
|
||||
func initClient(ctx context.Context, c *ServiceContext) error {
|
||||
Opened.Add(1)
|
||||
c.Client = &Client{}
|
||||
c.OnClose(func() error { Closed.Add(1); return nil })
|
||||
if c.Config.FailInit {
|
||||
return errors.New("client initialization failed")
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
235
cmd/go-omni/apigen/testdata/managed/consumer_test.go
vendored
Normal file
235
cmd/go-omni/apigen/testdata/managed/consumer_test.go
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"example.com/managed/internal/config"
|
||||
"example.com/managed/internal/handler"
|
||||
"example.com/managed/internal/svc"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
)
|
||||
|
||||
func application(t *testing.T) *omni.App {
|
||||
t.Helper()
|
||||
cfg := config.Default()
|
||||
cfg.Server.Log.Writer = io.Discard
|
||||
logger, err := logx.New(cfg.Server.Log)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = logger.Close() })
|
||||
deps, err := svc.New(context.Background(), logger, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = deps.Close() })
|
||||
app, err := handler.NewApp(deps)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = app.Close() })
|
||||
return app
|
||||
}
|
||||
func response(t *testing.T, app *omni.App, method, path string, body io.Reader, headers map[string]string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, body)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
out := httptest.NewRecorder()
|
||||
app.ServeHTTP(out, req)
|
||||
return out
|
||||
}
|
||||
func assertError(t *testing.T, out *httptest.ResponseRecorder, status, code int, message, field string) {
|
||||
t.Helper()
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(out.Body.Bytes(), &data); err != nil {
|
||||
t.Fatal(out.Body.String(), err)
|
||||
}
|
||||
value, exists := data["data"]
|
||||
if out.Code != status || data["code"] != float64(code) || data[field] != message || !exists || value != nil {
|
||||
t.Fatalf("unexpected error: %d %s", out.Code, out.Body.String())
|
||||
}
|
||||
}
|
||||
func TestGeneratedAppPolicies(t *testing.T) {
|
||||
app := application(t)
|
||||
assertError(t, response(t, app, "GET", "/check?kind=fixed", nil, nil), 400, 400002, "用户名格式不正确", "msg")
|
||||
assertError(t, response(t, app, "GET", "/check?kind=params", nil, nil), 400, 400003, "企业名称不能超过100个字符", "msg")
|
||||
assertError(t, response(t, app, "GET", "/check?kind=unknown", nil, nil), 500, 9999, "系统错误", "msg")
|
||||
assertError(t, response(t, app, "GET", "/other?kind=fixed", nil, nil), 400, 400002, "用户名格式不正确", "detail")
|
||||
for _, path := range []string{"/check", "/missing", "/callback"} {
|
||||
assertError(t, response(t, app, "GET", path, nil, map[string]string{"X-Fail": "1"}), 400, 400002, "用户名格式不正确", "msg")
|
||||
}
|
||||
assertError(t, response(t, app, "GET", "/missing", nil, nil), 404, 404001, "接口不存在", "msg")
|
||||
assertError(t, response(t, app, "POST", "/check", nil, nil), 405, 405001, "请求方法不支持", "msg")
|
||||
out := response(t, app, "GET", "/raw", nil, nil)
|
||||
if strings.TrimSpace(out.Body.String()) != `{"bytes":3}` {
|
||||
t.Fatal(out.Body.String())
|
||||
}
|
||||
}
|
||||
func TestGeneratedTransfers(t *testing.T) {
|
||||
app := application(t)
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
file, err := writer.CreateFormFile("file", "x.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = file.Write([]byte("hello"))
|
||||
_ = writer.Close()
|
||||
out := response(t, app, "POST", "/upload", &body, map[string]string{"Content-Type": writer.FormDataContentType()})
|
||||
if out.Code != 200 || !strings.Contains(out.Body.String(), `"bytes":5`) {
|
||||
t.Fatalf("upload: %d %s", out.Code, out.Body.String())
|
||||
}
|
||||
out = response(t, app, "POST", "/upload", strings.NewReader("bad"), map[string]string{"Content-Type": "application/json"})
|
||||
if out.Code != 415 {
|
||||
t.Fatal(out.Code, out.Body.String())
|
||||
}
|
||||
// Truncated uploads must use the project's public error catalog rather
|
||||
// than succeed or be mistaken for an internal failure.
|
||||
out = response(t, app, "POST", "/upload", strings.NewReader("--b\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\nprivate file"), map[string]string{"Content-Type": "multipart/form-data; boundary=b"})
|
||||
assertError(t, out, 400, 400004, "上传内容不完整", "msg")
|
||||
out = response(t, app, "POST", "/callback", strings.NewReader("{ \"exact\" : 123 }"), map[string]string{"X-Signature": "valid"})
|
||||
if out.Code != 200 || out.Body.String() != "ACK:{ \"exact\" : 123 }" || out.Header().Get("Content-Type") != "text/plain" {
|
||||
t.Fatal(out.Code, out.Body.String())
|
||||
}
|
||||
assertError(t, response(t, app, "POST", "/callback", strings.NewReader("{}"), nil), 400, 400002, "用户名格式不正确", "msg")
|
||||
// Unknown length must be limited by reads too, not just Content-Length.
|
||||
req := httptest.NewRequest("POST", "/callback", strings.NewReader(strings.Repeat("x", 65)))
|
||||
req.ContentLength = -1
|
||||
req.Header.Set("X-Signature", "valid")
|
||||
out = httptest.NewRecorder()
|
||||
app.ServeHTTP(out, req)
|
||||
assertError(t, out, 413, 413001, "请求体太大", "msg")
|
||||
before := svc.DownloadClosed.Load()
|
||||
out = response(t, app, "GET", "/download", nil, nil)
|
||||
if out.Body.String() != "file-content" || out.Header().Get("Content-Type") != "application/octet-stream" || !strings.Contains(out.Header().Get("Content-Disposition"), "attachment") || svc.DownloadClosed.Load() != before+1 {
|
||||
t.Fatal(out.Code, out.Body.String(), out.Header())
|
||||
}
|
||||
}
|
||||
func configFile(t *testing.T, address string, fail bool) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "config.json")
|
||||
data, _ := json.Marshal(map[string]any{"server": map[string]any{"address": address}, "failInit": fail})
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
func waitReady(t *testing.T, url string, done <-chan error) {
|
||||
t.Helper()
|
||||
client := http.Client{Timeout: 100 * time.Millisecond}
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
select {
|
||||
case err := <-done:
|
||||
t.Fatalf("startup failed: %v", err)
|
||||
default:
|
||||
}
|
||||
res, err := client.Get(url + "/health/ready")
|
||||
if err == nil {
|
||||
_ = res.Body.Close()
|
||||
if res.StatusCode == 200 {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("server did not become ready")
|
||||
}
|
||||
func TestGeneratedMainLifecycle(t *testing.T) {
|
||||
t.Run("initializationFailure", func(t *testing.T) {
|
||||
before := svc.Closed.Load()
|
||||
if err := run(context.Background(), configFile(t, "127.0.0.1:0", true)); err == nil {
|
||||
t.Fatal("failure hidden")
|
||||
}
|
||||
if svc.Closed.Load() != before+1 {
|
||||
t.Fatal("initialized client leaked or closed twice")
|
||||
}
|
||||
})
|
||||
t.Run("listenFailure", func(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
before := svc.Closed.Load()
|
||||
if err = run(context.Background(), configFile(t, listener.Addr().String(), false)); err == nil {
|
||||
t.Fatal("occupied listener accepted")
|
||||
}
|
||||
if svc.Closed.Load() != before+1 {
|
||||
t.Fatal("listen failure cleanup")
|
||||
}
|
||||
})
|
||||
t.Run("drainBeforeClose", func(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
address := listener.Addr().String()
|
||||
_ = listener.Close()
|
||||
svc.Closed.Store(0)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
done := make(chan error, 1)
|
||||
path := configFile(t, address, false)
|
||||
go func() { done <- run(ctx, path) }()
|
||||
base := "http://" + address
|
||||
waitReady(t, base, done)
|
||||
requestDone := make(chan error, 1)
|
||||
go func() {
|
||||
res, err := http.Get(base + "/slow")
|
||||
if err == nil {
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
_ = res.Body.Close()
|
||||
if res.StatusCode != 200 || !strings.Contains(string(data), `"bytes":1`) {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
requestDone <- err
|
||||
}()
|
||||
select {
|
||||
case <-svc.Started:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("request did not enter logic")
|
||||
}
|
||||
cancel()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
if svc.Closed.Load() != 0 {
|
||||
t.Fatal("dependency closed before drain")
|
||||
}
|
||||
close(svc.Release)
|
||||
select {
|
||||
case err := <-requestDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("request stuck")
|
||||
}
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("shutdown stuck")
|
||||
}
|
||||
if svc.Closed.Load() != 1 {
|
||||
t.Fatal("client closed more/less than once", svc.Closed.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
80
cmd/go-omni/apigen/testdata/managed/operations.go
vendored
Normal file
80
cmd/go-omni/apigen/testdata/managed/operations.go
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"example.com/managed/internal/svc"
|
||||
"example.com/managed/internal/types"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
)
|
||||
|
||||
func (l *Logic) Check(ctx context.Context, req *types.CheckReq) (*types.Receipt, error) {
|
||||
switch req.Kind {
|
||||
case "fixed":
|
||||
return nil, omnierrors.ByCode("INVALID")
|
||||
case "params":
|
||||
return nil, omnierrors.ByCode("TOO_LONG").WithParams(map[string]any{"label": "企业名称", "max": 100})
|
||||
case "unknown":
|
||||
return nil, omnierrors.ByCode("UNKNOWN").WithCause(errors.New("private cause"))
|
||||
}
|
||||
return &types.Receipt{Bytes: 42}, nil
|
||||
}
|
||||
func (l *Logic) Other(ctx context.Context, req *types.CheckReq) (*types.Receipt, error) {
|
||||
return l.Check(ctx, req)
|
||||
}
|
||||
func (l *Logic) RawJSON(context.Context) (*types.Receipt, error) {
|
||||
return &types.Receipt{Bytes: 3}, nil
|
||||
}
|
||||
func (l *Logic) Upload(ctx context.Context, body *httpx.Body) (*types.Receipt, error) {
|
||||
var total int64
|
||||
for {
|
||||
part, err := body.Parts.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := io.Copy(io.Discard, part)
|
||||
closeErr := part.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, closeErr
|
||||
}
|
||||
total += n
|
||||
}
|
||||
return &types.Receipt{Bytes: total}, nil
|
||||
}
|
||||
func (l *Logic) Callback(ctx context.Context, body *httpx.Body) (*httpx.Stream, error) {
|
||||
// The route's 64-byte limit bounds this example's memory use. A real signature
|
||||
// implementation can hash body.Raw incrementally and authenticate its headers.
|
||||
data, err := io.ReadAll(body.Raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Header.Get("X-Signature") != "valid" {
|
||||
return nil, omnierrors.ByCode("INVALID")
|
||||
}
|
||||
return &httpx.Stream{Content: io.NopCloser(strings.NewReader("ACK:" + string(data)))}, nil
|
||||
}
|
||||
func (l *Logic) Download(context.Context) (*httpx.Stream, error) {
|
||||
return &httpx.Stream{Content: &countedReader{Reader: strings.NewReader("file-content")}, Name: "测试.txt"}, nil
|
||||
}
|
||||
|
||||
type countedReader struct{ io.Reader }
|
||||
|
||||
func (c *countedReader) Close() error { svc.DownloadClosed.Add(1); return nil }
|
||||
func (l *Logic) Slow(context.Context) (*types.Receipt, error) {
|
||||
svc.Started <- struct{}{}
|
||||
<-svc.Release
|
||||
if svc.Closed.Load() != 0 {
|
||||
return nil, errors.New("dependency closed before drain")
|
||||
}
|
||||
return &types.Receipt{Bytes: 1}, nil
|
||||
}
|
||||
34
cmd/go-omni/apigen/testdata/managed/options.go
vendored
Normal file
34
cmd/go-omni/apigen/testdata/managed/options.go
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
)
|
||||
|
||||
func (c *ServiceContext) AppOptions() []omni.Option {
|
||||
return []omni.Option{omni.WithErrors(omnierrors.Catalog{
|
||||
DefaultStatus: 400,
|
||||
Definitions: map[string]omnierrors.Definition{
|
||||
"INVALID": {Code: 400002, Message: "用户名格式不正确"},
|
||||
"TOO_LONG": {Code: 400003, Message: "{label}不能超过{max}个字符", Params: []omnierrors.Parameter{{Name: "label", Type: "string"}, {Name: "max", Type: "int"}}},
|
||||
"INTERNAL_ERROR": {Status: 500, Code: 9999, Message: "系统错误"},
|
||||
"NOT_FOUND": {Status: 404, Code: 404001, Message: "接口不存在"},
|
||||
"METHOD_NOT_ALLOWED": {Status: 405, Code: 405001, Message: "请求方法不支持"},
|
||||
"BODY_TOO_LARGE": {Status: 413, Code: 413001, Message: "请求体太大"},
|
||||
"INVALID_MULTIPART": {Status: 400, Code: 400004, Message: "上传内容不完整"},
|
||||
},
|
||||
})}
|
||||
}
|
||||
func (c *ServiceContext) GlobalMiddleware() []httpx.Middleware {
|
||||
return []httpx.Middleware{
|
||||
func(next httpx.Handler) httpx.Handler {
|
||||
return func(c *httpx.Context) error {
|
||||
if c.Header("X-Fail") != "" {
|
||||
return omnierrors.ByCode("INVALID")
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user