feat: rewrite bus to prevent deadlocks
This commit is contained in:
282
pkg/http/blob.go
282
pkg/http/blob.go
@ -1,282 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
errorCodeForbidden = "forbidden"
|
||||
errorCodeInternalError = "internal-error"
|
||||
errorCodeBadRequest = "bad-request"
|
||||
errorCodeNotFound = "not-found"
|
||||
)
|
||||
|
||||
type uploadResponse struct {
|
||||
Bucket string `json:"bucket"`
|
||||
BlobID storage.BlobID `json:"blobId"`
|
||||
}
|
||||
|
||||
func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, h.uploadMaxFileSize)
|
||||
|
||||
if err := r.ParseMultipartForm(h.uploadMaxFileSize); err != nil {
|
||||
logger.Error(ctx, "could not parse multipart form", logger.CapturedE(errors.WithStack(err)))
|
||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, fileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not read form file", logger.CapturedE(errors.WithStack(err)))
|
||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var metadata map[string]any
|
||||
|
||||
rawMetadata := r.Form.Get("metadata")
|
||||
if rawMetadata != "" {
|
||||
if err := json.Unmarshal([]byte(rawMetadata), &metadata); err != nil {
|
||||
logger.Error(ctx, "could not parse metadata", logger.CapturedE(errors.WithStack(err)))
|
||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||
ContextKeyOriginRequest: r,
|
||||
})
|
||||
|
||||
requestMsg := blob.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
||||
|
||||
reply, err := h.bus.Request(ctx, requestMsg)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "upload reply", logger.F("reply", reply))
|
||||
|
||||
responseMsg, ok := reply.(*blob.MessageUploadResponse)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx, "unexpected upload response message",
|
||||
logger.F("message", reply),
|
||||
)
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !responseMsg.Allow {
|
||||
jsonError(w, http.StatusForbidden, errorCodeForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
res := &uploadResponse{
|
||||
Bucket: responseMsg.Bucket,
|
||||
BlobID: responseMsg.BlobID,
|
||||
}
|
||||
|
||||
if err := encoder.Encode(res); err != nil {
|
||||
panic(errors.Wrap(err, "could not encode upload response"))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
bucket := chi.URLParam(r, "bucket")
|
||||
blobID := chi.URLParam(r, "blobID")
|
||||
|
||||
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||
ContextKeyOriginRequest: r,
|
||||
})
|
||||
|
||||
requestMsg := blob.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
|
||||
|
||||
reply, err := h.bus.Request(ctx, requestMsg)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
replyMsg, ok := reply.(*blob.MessageDownloadResponse)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx, "unexpected download response message",
|
||||
logger.CapturedE(errors.WithStack(bus.ErrUnexpectedMessage)),
|
||||
logger.F("message", reply),
|
||||
)
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !replyMsg.Allow {
|
||||
jsonError(w, http.StatusForbidden, errorCodeForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if replyMsg.Blob == nil {
|
||||
jsonError(w, http.StatusNotFound, errorCodeNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := replyMsg.Blob.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close blob", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
http.ServeContent(w, r, string(replyMsg.BlobInfo.ID()), replyMsg.BlobInfo.ModTime(), replyMsg.Blob)
|
||||
}
|
||||
|
||||
func serveFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) {
|
||||
ctx := logger.With(r.Context(), logger.F("path", path))
|
||||
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "error while opening fs file", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
logger.Error(ctx, "error while closing fs file", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
logger.Error(ctx, "error while retrieving fs file stat", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
reader, ok := file.(io.ReadSeeker)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, path, info.ModTime(), reader)
|
||||
}
|
||||
|
||||
type jsonErrorResponse struct {
|
||||
Error jsonErr `json:"error"`
|
||||
}
|
||||
|
||||
type jsonErr struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func jsonError(w http.ResponseWriter, status int, code string) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
response := jsonErrorResponse{
|
||||
Error: jsonErr{
|
||||
Code: code,
|
||||
},
|
||||
}
|
||||
|
||||
if err := encoder.Encode(response); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
type uploadedFile struct {
|
||||
multipart.File
|
||||
header *multipart.FileHeader
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
// Stat implements fs.File
|
||||
func (f *uploadedFile) Stat() (fs.FileInfo, error) {
|
||||
return &uploadedFileInfo{
|
||||
header: f.header,
|
||||
modTime: f.modTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type uploadedFileInfo struct {
|
||||
header *multipart.FileHeader
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
// IsDir implements fs.FileInfo
|
||||
func (i *uploadedFileInfo) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ModTime implements fs.FileInfo
|
||||
func (i *uploadedFileInfo) ModTime() time.Time {
|
||||
return i.modTime
|
||||
}
|
||||
|
||||
// Mode implements fs.FileInfo
|
||||
func (i *uploadedFileInfo) Mode() fs.FileMode {
|
||||
return os.ModePerm
|
||||
}
|
||||
|
||||
// Name implements fs.FileInfo
|
||||
func (i *uploadedFileInfo) Name() string {
|
||||
return i.header.Filename
|
||||
}
|
||||
|
||||
// Size implements fs.FileInfo
|
||||
func (i *uploadedFileInfo) Size() int64 {
|
||||
return i.header.Size
|
||||
}
|
||||
|
||||
// Sys implements fs.FileInfo
|
||||
func (i *uploadedFileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ fs.File = &uploadedFile{}
|
||||
_ fs.FileInfo = &uploadedFileInfo{}
|
||||
)
|
@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
func (h *Handler) handleSDKClient(w http.ResponseWriter, r *http.Request) {
|
||||
serveFile(w, r, &sdk.FS, "client/dist/client.js")
|
||||
ServeFile(w, r, &sdk.FS, "client/dist/client.js")
|
||||
}
|
||||
|
||||
func (h *Handler) handleSDKClientMap(w http.ResponseWriter, r *http.Request) {
|
||||
serveFile(w, r, &sdk.FS, "client/dist/client.js.map")
|
||||
ServeFile(w, r, &sdk.FS, "client/dist/client.js.map")
|
||||
}
|
||||
|
||||
func (h *Handler) handleAppFiles(w http.ResponseWriter, r *http.Request) {
|
||||
|
55
pkg/http/context.go
Normal file
55
pkg/http/context.go
Normal file
@ -0,0 +1,55 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
var (
|
||||
contextKeyBus contextKey = "bus"
|
||||
contextKeyHTTPRequest contextKey = "httpRequest"
|
||||
contextKeyHTTPClient contextKey = "httpClient"
|
||||
)
|
||||
|
||||
func (h *Handler) contextMiddleware(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx = context.WithValue(ctx, contextKeyBus, h.bus)
|
||||
ctx = context.WithValue(ctx, contextKeyHTTPRequest, r)
|
||||
ctx = context.WithValue(ctx, contextKeyHTTPClient, h.httpClient)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func ContextBus(ctx context.Context) bus.Bus {
|
||||
return contextValue[bus.Bus](ctx, contextKeyBus)
|
||||
}
|
||||
|
||||
func ContextHTTPRequest(ctx context.Context) *http.Request {
|
||||
return contextValue[*http.Request](ctx, contextKeyHTTPRequest)
|
||||
}
|
||||
|
||||
func ContextHTTPClient(ctx context.Context) *http.Client {
|
||||
return contextValue[*http.Client](ctx, contextKeyHTTPClient)
|
||||
}
|
||||
|
||||
func contextValue[T any](ctx context.Context, key any) T {
|
||||
value, ok := ctx.Value(key).(T)
|
||||
if !ok {
|
||||
panic(errors.Errorf("could not find key '%v' on context", key))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
30
pkg/http/envelope.go
Normal file
30
pkg/http/envelope.go
Normal file
@ -0,0 +1,30 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
)
|
||||
|
||||
var (
|
||||
AddressIncomingMessage bus.Address = "http/incoming-message"
|
||||
AddressOutgoingMessage bus.Address = "http/outgoing-message"
|
||||
)
|
||||
|
||||
type IncomingMessage struct {
|
||||
Context context.Context
|
||||
Payload map[string]any
|
||||
}
|
||||
|
||||
func NewIncomingMessageEnvelope(ctx context.Context, payload map[string]any) bus.Envelope {
|
||||
return bus.NewEnvelope(AddressIncomingMessage, &IncomingMessage{ctx, payload})
|
||||
}
|
||||
|
||||
type OutgoingMessage struct {
|
||||
SessionID string
|
||||
Data any
|
||||
}
|
||||
|
||||
func NewOutgoingMessageEnvelope(sessionID string, data any) bus.Envelope {
|
||||
return bus.NewEnvelope(AddressOutgoingMessage, &OutgoingMessage{sessionID, data})
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (h *Handler) handleAppFetch(w http.ResponseWriter, r *http.Request) {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||
ContextKeyOriginRequest: r,
|
||||
})
|
||||
|
||||
rawURL := r.URL.Query().Get("url")
|
||||
|
||||
url, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
requestMsg := fetch.NewMessageFetchRequest(ctx, r.RemoteAddr, url)
|
||||
|
||||
reply, err := h.bus.Request(ctx, requestMsg)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve fetch request reply", logger.CapturedE(errors.WithStack(err)))
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "fetch reply", logger.F("reply", reply))
|
||||
|
||||
responseMsg, ok := reply.(*fetch.MessageFetchResponse)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx, "unexpected fetch response message",
|
||||
logger.F("message", reply),
|
||||
)
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !responseMsg.Allow {
|
||||
jsonError(w, http.StatusForbidden, errorCodeForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyReq, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not create proxy request",
|
||||
logger.CapturedE(errors.WithStack(err)),
|
||||
)
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for header, values := range r.Header {
|
||||
for _, value := range values {
|
||||
proxyReq.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
proxyReq.Header.Add("X-Forwarded-From", r.RemoteAddr)
|
||||
|
||||
res, err := h.httpClient.Do(proxyReq)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not execute proxy request",
|
||||
logger.CapturedE(errors.WithStack(err)),
|
||||
)
|
||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not close response body",
|
||||
logger.CapturedE(errors.WithStack(err)),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
for header, values := range res.Header {
|
||||
for _, value := range values {
|
||||
w.Header().Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
@ -24,10 +24,9 @@ type Handler struct {
|
||||
public http.Handler
|
||||
router chi.Router
|
||||
|
||||
sockjs http.Handler
|
||||
bus bus.Bus
|
||||
sockjsOpts sockjs.Options
|
||||
uploadMaxFileSize int64
|
||||
sockjs http.Handler
|
||||
bus bus.Bus
|
||||
sockjsOpts sockjs.Options
|
||||
|
||||
server *app.Server
|
||||
serverModuleFactories []app.ServerModuleFactory
|
||||
@ -57,10 +56,6 @@ func (h *Handler) Load(ctx context.Context, bdle bundle.Bundle) error {
|
||||
|
||||
server := app.NewServer(h.serverModuleFactories...)
|
||||
|
||||
if err := server.Load(serverMainScript, string(mainScript)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fs := bundle.NewFileSystem("public", bdle)
|
||||
public := HTML5Fileserver(fs)
|
||||
sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession)
|
||||
@ -69,7 +64,7 @@ func (h *Handler) Load(ctx context.Context, bdle bundle.Bundle) error {
|
||||
h.server.Stop()
|
||||
}
|
||||
|
||||
if err := server.Start(ctx); err != nil {
|
||||
if err := server.Start(ctx, serverMainScript, string(mainScript)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -90,7 +85,6 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||
router := chi.NewRouter()
|
||||
|
||||
handler := &Handler{
|
||||
uploadMaxFileSize: opts.UploadMaxFileSize,
|
||||
sockjsOpts: opts.SockJS,
|
||||
router: router,
|
||||
serverModuleFactories: opts.ServerModuleFactories,
|
||||
@ -108,19 +102,15 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||
r.Get("/client.js.map", handler.handleSDKClientMap)
|
||||
})
|
||||
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Post("/v1/upload", handler.handleAppUpload)
|
||||
r.Get("/v1/download/{bucket}/{blobID}", handler.handleAppDownload)
|
||||
|
||||
r.Get("/v1/fetch", handler.handleAppFetch)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(handler.contextMiddleware)
|
||||
for _, fn := range opts.HTTPMounts {
|
||||
r.Group(func(r chi.Router) {
|
||||
fn(r)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
for _, fn := range opts.HTTPMounts {
|
||||
r.Group(func(r chi.Router) {
|
||||
fn(r)
|
||||
})
|
||||
}
|
||||
|
||||
r.HandleFunc("/sock/*", handler.handleSockJS)
|
||||
})
|
||||
|
||||
|
@ -15,7 +15,6 @@ type HandlerOptions struct {
|
||||
Bus bus.Bus
|
||||
SockJS sockjs.Options
|
||||
ServerModuleFactories []app.ServerModuleFactory
|
||||
UploadMaxFileSize int64
|
||||
HTTPClient *http.Client
|
||||
HTTPMounts []func(r chi.Router)
|
||||
HTTPMiddlewares []func(next http.Handler) http.Handler
|
||||
@ -31,7 +30,6 @@ func defaultHandlerOptions() *HandlerOptions {
|
||||
Bus: memory.NewBus(),
|
||||
SockJS: sockjsOptions,
|
||||
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
||||
UploadMaxFileSize: 10 << (10 * 2), // 10Mb
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
},
|
||||
@ -60,12 +58,6 @@ func WithBus(bus bus.Bus) HandlerOptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func WithUploadMaxFileSize(size int64) HandlerOptionFunc {
|
||||
return func(opts *HandlerOptions) {
|
||||
opts.UploadMaxFileSize = size
|
||||
}
|
||||
}
|
||||
|
||||
func WithHTTPClient(client *http.Client) HandlerOptionFunc {
|
||||
return func(opts *HandlerOptions) {
|
||||
opts.HTTPClient = client
|
||||
|
@ -42,19 +42,18 @@ func (h *Handler) handleSockJSSession(sess sockjs.Session) {
|
||||
}
|
||||
}()
|
||||
|
||||
go h.handleServerMessages(ctx, sess)
|
||||
h.handleClientMessages(ctx, sess)
|
||||
go h.handleOutgoingMessages(ctx, sess)
|
||||
h.handleIncomingMessages(ctx, sess)
|
||||
}
|
||||
|
||||
func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session) {
|
||||
messages, err := h.bus.Subscribe(ctx, module.MessageNamespaceServer)
|
||||
func (h *Handler) handleOutgoingMessages(ctx context.Context, sess sockjs.Session) {
|
||||
envelopes, err := h.bus.Subscribe(ctx, AddressOutgoingMessage)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Close messages subscriber
|
||||
h.bus.Unsubscribe(ctx, module.MessageNamespaceServer, messages)
|
||||
h.bus.Unsubscribe(AddressOutgoingMessage, envelopes)
|
||||
|
||||
logger.Debug(ctx, "unsubscribed")
|
||||
|
||||
@ -72,26 +71,22 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case msg := <-messages:
|
||||
serverMessage, ok := msg.(*module.ServerMessage)
|
||||
case env := <-envelopes:
|
||||
outgoingMessage, ok := env.Message().(*OutgoingMessage)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unexpected server message",
|
||||
logger.F("message", msg),
|
||||
"unexpected outgoing message",
|
||||
logger.F("message", env.Message()),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
sessionID := module.ContextValue[string](serverMessage.Context, ContextKeySessionID)
|
||||
|
||||
isDest := sessionID == "" || sessionID == sess.ID()
|
||||
isDest := outgoingMessage.SessionID == "" || outgoingMessage.SessionID == sess.ID()
|
||||
if !isDest {
|
||||
continue
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(serverMessage.Data)
|
||||
payload, err := json.Marshal(outgoingMessage.Data)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
@ -132,7 +127,7 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session) {
|
||||
func (h *Handler) handleIncomingMessages(ctx context.Context, sess sockjs.Session) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@ -145,7 +140,7 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||
|
||||
data, err := sess.RecvCtx(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sockjs.ErrSessionNotOpen) {
|
||||
if errors.Is(err, sockjs.ErrSessionNotOpen) || errors.Is(err, context.Canceled) {
|
||||
break
|
||||
}
|
||||
|
||||
@ -174,7 +169,7 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||
switch {
|
||||
|
||||
case message.Type == WebsocketMessageTypeMessage:
|
||||
var payload map[string]interface{}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(message.Payload, &payload); err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
@ -191,21 +186,19 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||
ContextKeyOriginRequest: sess.Request(),
|
||||
})
|
||||
|
||||
clientMessage := module.NewClientMessage(ctx, payload)
|
||||
incomingMessage := NewIncomingMessageEnvelope(ctx, payload)
|
||||
|
||||
logger.Debug(ctx, "publishing new client message", logger.F("message", clientMessage))
|
||||
logger.Debug(ctx, "publishing new incoming message", logger.F("message", incomingMessage))
|
||||
|
||||
if err := h.bus.Publish(ctx, clientMessage); err != nil {
|
||||
if err := h.bus.Publish(incomingMessage); err != nil {
|
||||
logger.Error(ctx, "could not publish message",
|
||||
logger.CapturedE(errors.WithStack(err)),
|
||||
logger.F("message", clientMessage),
|
||||
logger.F("message", incomingMessage),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "new client message published", logger.F("message", clientMessage))
|
||||
|
||||
default:
|
||||
logger.Error(
|
||||
ctx,
|
||||
|
82
pkg/http/util.go
Normal file
82
pkg/http/util.go
Normal file
@ -0,0 +1,82 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeForbidden = "forbidden"
|
||||
ErrCodeInternalError = "internal-error"
|
||||
ErrCodeBadRequest = "bad-request"
|
||||
ErrCodeNotFound = "not-found"
|
||||
)
|
||||
|
||||
type jsonErrorResponse struct {
|
||||
Error jsonErr `json:"error"`
|
||||
}
|
||||
|
||||
type jsonErr struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func JSONError(w http.ResponseWriter, status int, code string) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
response := jsonErrorResponse{
|
||||
Error: jsonErr{
|
||||
Code: code,
|
||||
},
|
||||
}
|
||||
|
||||
if err := encoder.Encode(response); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
func ServeFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) {
|
||||
ctx := logger.With(r.Context(), logger.F("path", path))
|
||||
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "error while opening fs file", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
logger.Error(ctx, "error while closing fs file", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
logger.Error(ctx, "error while retrieving fs file stat", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
reader, ok := file.(io.ReadSeeker)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, path, info.ModTime(), reader)
|
||||
}
|
Reference in New Issue
Block a user