Add batch endpoint (#19)

* add batch endpoint.

* keep error code.

* use utils.HttpSuccess.

* add batch to openapi.

* body omitempty.
This commit is contained in:
Miroslav Šedivý 2023-01-09 23:18:47 +01:00 committed by GitHub
parent fb8462b56a
commit 3c3042d691
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 185 additions and 0 deletions

123
internal/http/batch.go Normal file
View File

@ -0,0 +1,123 @@
package http
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"github.com/demodesk/neko/pkg/types"
"github.com/demodesk/neko/pkg/utils"
)
type BatchRequest struct {
Path string `json:"path"`
Method string `json:"method"`
Body json.RawMessage `json:"body,omitempty"`
}
type BatchResponse struct {
Path string `json:"path"`
Method string `json:"method"`
Body json.RawMessage `json:"body,omitempty"`
Status int `json:"status"`
}
func (b *BatchResponse) Error(httpErr *utils.HTTPError) (err error) {
b.Body, err = json.Marshal(httpErr)
b.Status = httpErr.Code
return
}
type batchHandler struct {
Router types.Router
PathPrefix string
Excluded []string
}
func (b *batchHandler) Handle(w http.ResponseWriter, r *http.Request) error {
var requests []BatchRequest
if err := json.NewDecoder(r.Body).Decode(&requests); err != nil {
return err
}
responses := make([]BatchResponse, len(requests))
for i, request := range requests {
res := BatchResponse{
Path: request.Path,
Method: request.Method,
}
if !strings.HasPrefix(request.Path, b.PathPrefix) {
res.Error(utils.HttpBadRequest("this path is not allowed in batch requests"))
responses[i] = res
continue
}
if exists, _ := utils.ArrayIn(request.Path, b.Excluded); exists {
res.Error(utils.HttpBadRequest("this path is excluded from batch requests"))
responses[i] = res
continue
}
// prepare request
req, err := http.NewRequest(request.Method, request.Path, bytes.NewBuffer(request.Body))
if err != nil {
return err
}
// copy headers
for k, vv := range r.Header {
for _, v := range vv {
req.Header.Add(k, v)
}
}
// execute request
rr := newResponseRecorder()
b.Router.ServeHTTP(rr, req)
// read response
body, err := io.ReadAll(rr.Body)
if err != nil {
return err
}
// write response
responses[i] = BatchResponse{
Path: request.Path,
Method: request.Method,
Body: body,
Status: rr.Code,
}
}
return utils.HttpSuccess(w, responses)
}
type responseRecorder struct {
Code int
HeaderMap http.Header
Body *bytes.Buffer
}
func newResponseRecorder() *responseRecorder {
return &responseRecorder{
Code: http.StatusOK,
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
}
func (w *responseRecorder) Header() http.Header {
return w.HeaderMap
}
func (w *responseRecorder) Write(b []byte) (int, error) {
return w.Body.Write(b)
}
func (w *responseRecorder) WriteHeader(code int) {
w.Code = code
}

View File

@ -42,6 +42,16 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c
return config.AllowOrigin(r.Header.Get("Origin")) return config.AllowOrigin(r.Header.Get("Origin"))
})) }))
batch := batchHandler{
Router: router,
PathPrefix: "/api",
Excluded: []string{
"/api/batch", // do not allow batchception
"/api/ws",
},
}
router.Post("/api/batch", batch.Handle)
router.Get("/health", func(w http.ResponseWriter, r *http.Request) error { router.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
_, err := w.Write([]byte("true")) _, err := w.Write([]byte("true"))
return err return err

View File

@ -38,6 +38,28 @@ paths:
'200': '200':
description: OK description: OK
/api/batch:
post:
summary: batch
operationId: batch
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BatchResponse'
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BatchRequest'
required: true
# #
# session # session
# #
@ -930,6 +952,36 @@ components:
message: message:
type: string type: string
BatchRequest:
type: object
properties:
path:
type: string
method:
type: string
enum:
- GET
- POST
- DELETE
body:
description: Request body
BatchResponse:
type: object
properties:
path:
type: string
method:
type: string
enum:
- GET
- POST
- DELETE
body:
description: Response body
status:
type: integer
# #
# session # session
# #