Enable returning partial HTTPResponses
This commit is contained in:
parent
47c61b02f7
commit
f4ccd53ba5
|
@ -2,7 +2,6 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -151,14 +150,14 @@ func invokeHookSync(typ hooks.HookType, event handler.HookEvent) (httpRes handle
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s hook failed: %s", typ, err)
|
//err = fmt.Errorf("%s hook failed: %s", typ, err)
|
||||||
logEv(stderr, "HookInvocationError", "type", string(typ), "id", id, "error", err.Error())
|
logEv(stderr, "HookInvocationError", "type", string(typ), "id", id, "error", err.Error())
|
||||||
MetricsHookErrorsTotal.WithLabelValues(string(typ)).Add(1)
|
MetricsHookErrorsTotal.WithLabelValues(string(typ)).Add(1)
|
||||||
} else if Flags.VerboseOutput {
|
} else if Flags.VerboseOutput {
|
||||||
logEv(stdout, "HookInvocationFinish", "type", string(typ), "id", id)
|
logEv(stdout, "HookInvocationFinish", "type", string(typ), "id", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDEA: PreHooks work like this: error return value does not carry HTTP response information
|
// IDEA: PreHooks work like this: error return value does carry HTTP response information for error
|
||||||
// Instead the additional HTTP response return value
|
// Instead the additional HTTP response return value
|
||||||
|
|
||||||
httpRes = hookRes.HTTPResponse
|
httpRes = hookRes.HTTPResponse
|
||||||
|
@ -171,12 +170,10 @@ func invokeHookSync(typ hooks.HookType, event handler.HookEvent) (httpRes handle
|
||||||
// If the hook response includes the instruction to reject the upload, reuse the error code
|
// If the hook response includes the instruction to reject the upload, reuse the error code
|
||||||
// and message from ErrUploadRejectedByServer, but also include custom HTTP response values
|
// and message from ErrUploadRejectedByServer, but also include custom HTTP response values
|
||||||
if typ == hooks.HookPreCreate && hookRes.RejectUpload {
|
if typ == hooks.HookPreCreate && hookRes.RejectUpload {
|
||||||
// TODO: Merge httpRes with default of ErrUploadRejected, so we always have a response code.
|
err := handler.ErrUploadRejectedByServer
|
||||||
return httpRes, handler.Error{
|
err.HTTPResponse = err.HTTPResponse.MergeWith(httpRes)
|
||||||
ErrorCode: handler.ErrUploadRejectedByServer.ErrorCode,
|
|
||||||
Message: handler.ErrUploadRejectedByServer.Message,
|
return httpRes, err
|
||||||
HTTPResponse: httpRes,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if typ == hooks.HookPostReceive && hookRes.StopUpload {
|
if typ == hooks.HookPostReceive && hookRes.StopUpload {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package hooks
|
package hooks
|
||||||
|
|
||||||
|
// TODO: Move hooks into a package in /pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tus/tusd/pkg/handler"
|
"github.com/tus/tusd/pkg/handler"
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,8 +7,12 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/hashicorp/go-plugin"
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/tus/tusd/pkg/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: When the tusd process stops, the plugin does not get properly killed
|
||||||
|
// and lives on as a zombie process.
|
||||||
|
|
||||||
type PluginHook struct {
|
type PluginHook struct {
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
|
@ -54,8 +58,8 @@ func (h *PluginHook) InvokeHook(req HookRequest) (HookResponse, error) {
|
||||||
// directory. It is a UX feature, not a security feature.
|
// directory. It is a UX feature, not a security feature.
|
||||||
var handshakeConfig = plugin.HandshakeConfig{
|
var handshakeConfig = plugin.HandshakeConfig{
|
||||||
ProtocolVersion: 1,
|
ProtocolVersion: 1,
|
||||||
MagicCookieKey: "BASIC_PLUGIN",
|
MagicCookieKey: "TUSD_PLUGIN",
|
||||||
MagicCookieValue: "hello",
|
MagicCookieValue: "yes",
|
||||||
}
|
}
|
||||||
|
|
||||||
// pluginMap is the map of plugins we can dispense.
|
// pluginMap is the map of plugins we can dispense.
|
||||||
|
@ -63,19 +67,35 @@ var pluginMap = map[string]plugin.Plugin{
|
||||||
"hookHandler": &HookHandlerPlugin{},
|
"hookHandler": &HookHandlerPlugin{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Explain, mention that it is internal only
|
||||||
|
// TODO: Do we actually need this? Maybe not...
|
||||||
|
type InvokeHookRPCAnswer struct {
|
||||||
|
HookResponse HookResponse
|
||||||
|
TusdError *handler.Error // Why is TusdError a pointer
|
||||||
|
}
|
||||||
|
|
||||||
// Here is an implementation that talks over RPC
|
// Here is an implementation that talks over RPC
|
||||||
type HookHandlerRPC struct{ client *rpc.Client }
|
type HookHandlerRPC struct{ client *rpc.Client }
|
||||||
|
|
||||||
func (g *HookHandlerRPC) Setup() error {
|
func (g *HookHandlerRPC) Setup() error {
|
||||||
var res interface{}
|
var res interface{}
|
||||||
err := g.client.Call("Plugin.Setup", new(interface{}), &res)
|
err := g.client.Call("Plugin.Setup", new(interface{}), &res)
|
||||||
fmt.Println("after Setup")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *HookHandlerRPC) InvokeHook(req HookRequest) (res HookResponse, err error) {
|
func (g *HookHandlerRPC) InvokeHook(req HookRequest) (HookResponse, error) {
|
||||||
err = g.client.Call("Plugin.InvokeHook", req, &res)
|
var answer InvokeHookRPCAnswer
|
||||||
return res, err
|
err := g.client.Call("Plugin.InvokeHook", req, &answer)
|
||||||
|
fmt.Printf("Client: %#v\n", answer.TusdError)
|
||||||
|
if err != nil {
|
||||||
|
return answer.HookResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if answer.TusdError != nil {
|
||||||
|
return answer.HookResponse, *answer.TusdError
|
||||||
|
}
|
||||||
|
|
||||||
|
return answer.HookResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here is the RPC server that HookHandlerRPC talks to, conforming to
|
// Here is the RPC server that HookHandlerRPC talks to, conforming to
|
||||||
|
@ -89,9 +109,21 @@ func (s *HookHandlerRPCServer) Setup(args interface{}, resp *interface{}) error
|
||||||
return s.Impl.Setup()
|
return s.Impl.Setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HookHandlerRPCServer) InvokeHook(args HookRequest, resp *HookResponse) (err error) {
|
func (s *HookHandlerRPCServer) InvokeHook(args HookRequest, answer *InvokeHookRPCAnswer) error {
|
||||||
*resp, err = s.Impl.InvokeHook(args)
|
resp, err := s.Impl.InvokeHook(args)
|
||||||
return err
|
if err != nil {
|
||||||
|
|
||||||
|
if tusdErr, ok := err.(handler.Error); ok {
|
||||||
|
answer.TusdError = &tusdErr
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
answer.HookResponse = resp
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the implementation of plugin.Plugin so we can serve/consume this
|
// This is the implementation of plugin.Plugin so we can serve/consume this
|
||||||
|
|
|
@ -26,6 +26,14 @@ func (g *MyHookHandler) InvokeHook(req hooks.HookRequest) (res hooks.HookRespons
|
||||||
|
|
||||||
if req.Type == hooks.HookPreCreate {
|
if req.Type == hooks.HookPreCreate {
|
||||||
res.HTTPResponse.Headers["X-From-Pre-Create"] = "hello"
|
res.HTTPResponse.Headers["X-From-Pre-Create"] = "hello"
|
||||||
|
|
||||||
|
if req.Event.Upload.Size > 10 {
|
||||||
|
res.HTTPResponse.StatusCode = 413
|
||||||
|
res.HTTPResponse.Body = `{"error":"upload size is too large"}`
|
||||||
|
res.RejectUpload = true
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Type == hooks.HookPreFinish {
|
if req.Type == hooks.HookPreFinish {
|
||||||
|
@ -42,8 +50,8 @@ func (g *MyHookHandler) InvokeHook(req hooks.HookRequest) (res hooks.HookRespons
|
||||||
// directory. It is a UX feature, not a security feature.
|
// directory. It is a UX feature, not a security feature.
|
||||||
var handshakeConfig = plugin.HandshakeConfig{
|
var handshakeConfig = plugin.HandshakeConfig{
|
||||||
ProtocolVersion: 1,
|
ProtocolVersion: 1,
|
||||||
MagicCookieKey: "BASIC_PLUGIN",
|
MagicCookieKey: "TUSD_PLUGIN",
|
||||||
MagicCookieValue: "hello",
|
MagicCookieValue: "yes",
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
@ -118,18 +118,33 @@ func (resp HTTPResponse) writeTo(w http.ResponseWriter) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resp *HTTPResponse) MergeWith(resp2 HTTPResponse) {
|
func (resp1 HTTPResponse) MergeWith(resp2 HTTPResponse) HTTPResponse {
|
||||||
if resp2.StatusCode != 0 {
|
// Clone the response 1 and use it as a basis
|
||||||
resp.StatusCode = resp2.StatusCode
|
newResp := resp1
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range resp2.Headers {
|
// Take the status code and body from response 2 to
|
||||||
resp.Headers[key] = value
|
// overwrite values from response 1.
|
||||||
|
if resp2.StatusCode != 0 {
|
||||||
|
newResp.StatusCode = resp2.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resp2.Body) > 0 {
|
if len(resp2.Body) > 0 {
|
||||||
resp.Body = resp2.Body
|
newResp.Body = resp2.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For the headers, me must make a new map to avoid writing
|
||||||
|
// into the header map from response 1.
|
||||||
|
newResp.Headers = make(HTTPHeaders, len(resp1.Headers)+len(resp2.Headers))
|
||||||
|
|
||||||
|
for key, value := range resp1.Headers {
|
||||||
|
newResp.Headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range resp2.Headers {
|
||||||
|
newResp.Headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return newResp
|
||||||
}
|
}
|
||||||
|
|
||||||
// HookEvent represents an event from tusd which can be handled by the application.
|
// HookEvent represents an event from tusd which can be handled by the application.
|
||||||
|
@ -400,7 +415,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
|
||||||
handler.sendError(w, r, err)
|
handler.sendError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.MergeWith(resp2)
|
resp = resp.MergeWith(resp2)
|
||||||
}
|
}
|
||||||
|
|
||||||
upload, err := handler.composer.Core.NewUpload(ctx, info)
|
upload, err := handler.composer.Core.NewUpload(ctx, info)
|
||||||
|
@ -770,7 +785,7 @@ func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, uplo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
resp.MergeWith(resp2)
|
resp = resp.MergeWith(resp2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1035,7 +1050,7 @@ func (handler *UnroutedHandler) sendError(w http.ResponseWriter, r *http.Request
|
||||||
func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, resp HTTPResponse) {
|
func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, resp HTTPResponse) {
|
||||||
resp.writeTo(w)
|
resp.writeTo(w)
|
||||||
|
|
||||||
handler.log("ResponseOutgoing", "status", strconv.Itoa(resp.StatusCode), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r))
|
handler.log("ResponseOutgoing", "status", strconv.Itoa(resp.StatusCode), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r), "body", resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make an absolute URLs to the given upload id. If the base path is absolute
|
// Make an absolute URLs to the given upload id. If the base path is absolute
|
||||||
|
|
Loading…
Reference in New Issue