Rework error type from interface to struct

This commit is contained in:
Marius 2021-11-27 21:38:26 +01:00
parent 0513a59e0b
commit 93187d760c
9 changed files with 197 additions and 195 deletions

View File

@ -46,7 +46,6 @@ var Flags struct {
GrpcHooksEndpoint string GrpcHooksEndpoint string
GrpcHooksRetry int GrpcHooksRetry int
GrpcHooksBackoff int GrpcHooksBackoff int
HooksStopUploadCode int
EnabledHooks []hooks.HookType EnabledHooks []hooks.HookType
ShowVersion bool ShowVersion bool
ExposeMetrics bool ExposeMetrics bool
@ -98,7 +97,6 @@ func ParseFlags() {
flag.StringVar(&Flags.GrpcHooksEndpoint, "hooks-grpc", "", "An gRPC endpoint to which hook events will be sent to") flag.StringVar(&Flags.GrpcHooksEndpoint, "hooks-grpc", "", "An gRPC endpoint to which hook events will be sent to")
flag.IntVar(&Flags.GrpcHooksRetry, "hooks-grpc-retry", 3, "Number of times to retry on a server error or network timeout") flag.IntVar(&Flags.GrpcHooksRetry, "hooks-grpc-retry", 3, "Number of times to retry on a server error or network timeout")
flag.IntVar(&Flags.GrpcHooksBackoff, "hooks-grpc-backoff", 1, "Number of seconds to wait before retrying each retry") flag.IntVar(&Flags.GrpcHooksBackoff, "hooks-grpc-backoff", 1, "Number of seconds to wait before retrying each retry")
flag.IntVar(&Flags.HooksStopUploadCode, "hooks-stop-code", 0, "Return code from post-receive hook which causes tusd to stop and delete the current upload. A zero value means that no uploads will be stopped")
flag.BoolVar(&Flags.ShowVersion, "version", false, "Print tusd version information") flag.BoolVar(&Flags.ShowVersion, "version", false, "Print tusd version information")
flag.BoolVar(&Flags.ExposeMetrics, "expose-metrics", true, "Expose metrics about tusd usage") flag.BoolVar(&Flags.ExposeMetrics, "expose-metrics", true, "Expose metrics about tusd usage")
flag.StringVar(&Flags.MetricsPath, "metrics-path", "/metrics", "Path under which the metrics endpoint will be accessible") flag.StringVar(&Flags.MetricsPath, "metrics-path", "/metrics", "Path under which the metrics endpoint will be accessible")

View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -20,27 +21,12 @@ func hookTypeInSlice(a hooks.HookType, list []hooks.HookType) bool {
return false return false
} }
func hookCallback(typ hooks.HookType, info handler.HookEvent) error { func preCreateCallback(event handler.HookEvent) (handler.HTTPResponse, error) {
if output, err := invokeHookSync(typ, info, true); err != nil { return invokeHookSync(hooks.HookPreCreate, event)
if hookErr, ok := err.(hooks.HookError); ok {
return hooks.NewHookError(
fmt.Errorf("%s hook failed: %s", typ, err),
hookErr.StatusCode(),
hookErr.Body(),
)
}
return fmt.Errorf("%s hook failed: %s\n%s", typ, err, string(output))
}
return nil
} }
func preCreateCallback(info handler.HookEvent) error { func preFinishCallback(event handler.HookEvent) (handler.HTTPResponse, error) {
return hookCallback(hooks.HookPreCreate, info) return invokeHookSync(hooks.HookPreFinish, event)
}
func preFinishCallback(info handler.HookEvent) error {
return hookCallback(hooks.HookPreFinish, info)
} }
func SetupHookMetrics() { func SetupHookMetrics() {
@ -59,13 +45,14 @@ func SetupHookMetrics() {
} }
func SetupPreHooks(config *handler.Config) error { func SetupPreHooks(config *handler.Config) error {
if Flags.FileHooksDir != "" { // if Flags.FileHooksDir != "" {
stdout.Printf("Using '%s' for hooks", Flags.FileHooksDir) // stdout.Printf("Using '%s' for hooks", Flags.FileHooksDir)
hookHandler = &hooks.FileHook{ // hookHandler = &hooks.FileHook{
Directory: Flags.FileHooksDir, // Directory: Flags.FileHooksDir,
} // }
} else if Flags.HttpHooksEndpoint != "" { // } else
if Flags.HttpHooksEndpoint != "" {
stdout.Printf("Using '%s' as the endpoint for hooks", Flags.HttpHooksEndpoint) stdout.Printf("Using '%s' as the endpoint for hooks", Flags.HttpHooksEndpoint)
hookHandler = &hooks.HttpHook{ hookHandler = &hooks.HttpHook{
@ -74,14 +61,14 @@ func SetupPreHooks(config *handler.Config) error {
Backoff: Flags.HttpHooksBackoff, Backoff: Flags.HttpHooksBackoff,
ForwardHeaders: strings.Split(Flags.HttpHooksForwardHeaders, ","), ForwardHeaders: strings.Split(Flags.HttpHooksForwardHeaders, ","),
} }
} else if Flags.GrpcHooksEndpoint != "" { // } else if Flags.GrpcHooksEndpoint != "" {
stdout.Printf("Using '%s' as the endpoint for gRPC hooks", Flags.GrpcHooksEndpoint) // stdout.Printf("Using '%s' as the endpoint for gRPC hooks", Flags.GrpcHooksEndpoint)
hookHandler = &hooks.GrpcHook{ // hookHandler = &hooks.GrpcHook{
Endpoint: Flags.GrpcHooksEndpoint, // Endpoint: Flags.GrpcHooksEndpoint,
MaxRetries: Flags.GrpcHooksRetry, // MaxRetries: Flags.GrpcHooksRetry,
Backoff: Flags.GrpcHooksBackoff, // Backoff: Flags.GrpcHooksBackoff,
} // }
} else { } else {
return nil return nil
} }
@ -107,35 +94,35 @@ func SetupPostHooks(handler *handler.Handler) {
go func() { go func() {
for { for {
select { select {
case info := <-handler.CompleteUploads: case event := <-handler.CompleteUploads:
invokeHookAsync(hooks.HookPostFinish, info) invokeHookAsync(hooks.HookPostFinish, event)
case info := <-handler.TerminatedUploads: case event := <-handler.TerminatedUploads:
invokeHookAsync(hooks.HookPostTerminate, info) invokeHookAsync(hooks.HookPostTerminate, event)
case info := <-handler.UploadProgress: case event := <-handler.UploadProgress:
invokeHookAsync(hooks.HookPostReceive, info) invokeHookAsync(hooks.HookPostReceive, event)
case info := <-handler.CreatedUploads: case event := <-handler.CreatedUploads:
invokeHookAsync(hooks.HookPostCreate, info) invokeHookAsync(hooks.HookPostCreate, event)
} }
} }
}() }()
} }
func invokeHookAsync(typ hooks.HookType, info handler.HookEvent) { func invokeHookAsync(typ hooks.HookType, event handler.HookEvent) {
go func() { go func() {
// Error handling is taken care by the function. // Error handling is taken care by the function.
_, _ = invokeHookSync(typ, info, false) _, _ = invokeHookSync(typ, event)
}() }()
} }
func invokeHookSync(typ hooks.HookType, info handler.HookEvent, captureOutput bool) ([]byte, error) { func invokeHookSync(typ hooks.HookType, event handler.HookEvent) (httpRes handler.HTTPResponse, err error) {
if !hookTypeInSlice(typ, Flags.EnabledHooks) { if !hookTypeInSlice(typ, Flags.EnabledHooks) {
return nil, nil return httpRes, nil
} }
MetricsHookInvocationsTotal.WithLabelValues(string(typ)).Add(1) MetricsHookInvocationsTotal.WithLabelValues(string(typ)).Add(1)
id := info.Upload.ID id := event.Upload.ID
size := info.Upload.Size size := event.Upload.Size
switch typ { switch typ {
case hooks.HookPostFinish: case hooks.HookPostFinish:
@ -145,28 +132,52 @@ func invokeHookSync(typ hooks.HookType, info handler.HookEvent, captureOutput bo
} }
if hookHandler == nil { if hookHandler == nil {
return nil, nil return httpRes, nil
} }
name := string(typ)
if Flags.VerboseOutput { if Flags.VerboseOutput {
logEv(stdout, "HookInvocationStart", "type", name, "id", id) logEv(stdout, "HookInvocationStart", "type", string(typ), "id", id)
} }
output, returnCode, err := hookHandler.InvokeHook(typ, info, captureOutput) hookRes, err := hookHandler.InvokeHook(hooks.HookRequest{
Type: typ,
Event: event,
})
if err != nil { if err != nil {
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)
} }
if typ == hooks.HookPostReceive && Flags.HooksStopUploadCode != 0 && Flags.HooksStopUploadCode == returnCode { // IDEA: PreHooks work like this: error return value does not carry HTTP response information
logEv(stdout, "HookStopUpload", "id", id) // Instead the additional HTTP response return value
info.Upload.StopUpload() httpRes = hookRes.HTTPResponse
if hookRes.Error != "" {
// TODO: Is this actually useful?
return httpRes, errors.New(hookRes.Error)
} }
return output, err // 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
if typ == hooks.HookPreCreate && hookRes.RejectUpload {
return httpRes, handler.Error{
ErrorCode: handler.ErrUploadRejectedByServer.ErrorCode,
Message: handler.ErrUploadRejectedByServer.Message,
HTTPResponse: httpRes,
}
}
if typ == hooks.HookPostReceive && hookRes.StopUpload {
logEv(stdout, "HookStopUpload", "id", id)
// TODO: Control response for PATCH request
event.Upload.StopUpload()
}
return httpRes, err
} }

View File

@ -6,7 +6,23 @@ import (
type HookHandler interface { type HookHandler interface {
Setup() error Setup() error
InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) InvokeHook(req HookRequest) (res HookResponse, err error)
}
type HookRequest struct {
Type HookType
Event handler.HookEvent
}
type HookResponse struct {
// Error indicates whether a fault occurred while processing the hook request.
// If Error is an empty string, no fault is assumed.
Error string
HTTPResponse handler.HTTPResponse
RejectUpload bool
StopUpload bool
} }
type HookType string type HookType string
@ -21,29 +37,3 @@ const (
) )
var AvailableHooks []HookType = []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish} var AvailableHooks []HookType = []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish}
type hookDataStore struct {
handler.DataStore
}
type HookError struct {
error
statusCode int
body []byte
}
func NewHookError(err error, statusCode int, body []byte) HookError {
return HookError{err, statusCode, body}
}
func (herr HookError) StatusCode() int {
return herr.statusCode
}
func (herr HookError) Body() []byte {
return herr.body
}
func (herr HookError) Error() string {
return herr.error.Error()
}

View File

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/tus/tusd/pkg/handler"
"github.com/sethgrid/pester" "github.com/sethgrid/pester"
) )
@ -18,35 +16,11 @@ type HttpHook struct {
MaxRetries int MaxRetries int
Backoff int Backoff int
ForwardHeaders []string ForwardHeaders []string
client *pester.Client
} }
func (_ HttpHook) Setup() error { func (h *HttpHook) Setup() error {
return nil
}
func (h HttpHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) {
jsonInfo, err := json.Marshal(info)
if err != nil {
return nil, 0, err
}
req, err := http.NewRequest("POST", h.Endpoint, bytes.NewBuffer(jsonInfo))
if err != nil {
return nil, 0, err
}
for _, k := range h.ForwardHeaders {
// Lookup the Canonicalised version of the specified header
if vals, ok := info.HTTPRequest.Header[http.CanonicalHeaderKey(k)]; ok {
// but set the case specified by the user
req.Header[k] = vals
}
}
req.Header.Set("Hook-Name", string(typ))
req.Header.Set("Content-Type", "application/json")
// TODO: Can we initialize this in Setup()?
// Use linear backoff strategy with the user defined values. // Use linear backoff strategy with the user defined values.
client := pester.New() client := pester.New()
client.KeepLog = true client.KeepLog = true
@ -55,24 +29,51 @@ func (h HttpHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput
return time.Duration(h.Backoff) * time.Second return time.Duration(h.Backoff) * time.Second
} }
resp, err := client.Do(req) h.client = client
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) return nil
if err != nil { }
return nil, 0, err
} func (h HttpHook) InvokeHook(hookReq HookRequest) (hookRes HookResponse, err error) {
jsonInfo, err := json.Marshal(hookReq)
if resp.StatusCode >= http.StatusBadRequest { if err != nil {
return body, resp.StatusCode, NewHookError(fmt.Errorf("endpoint returned: %s", resp.Status), resp.StatusCode, body) return hookRes, err
} }
if captureOutput { httpReq, err := http.NewRequest("POST", h.Endpoint, bytes.NewBuffer(jsonInfo))
return body, resp.StatusCode, err if err != nil {
} return hookRes, err
}
return nil, resp.StatusCode, err
for _, k := range h.ForwardHeaders {
// Lookup the Canonicalised version of the specified header
if vals, ok := hookReq.Event.HTTPRequest.Header[http.CanonicalHeaderKey(k)]; ok {
// but set the case specified by the user
httpReq.Header[k] = vals
}
}
httpReq.Header.Set("Content-Type", "application/json")
httpRes, err := h.client.Do(httpReq)
if err != nil {
return hookRes, err
}
defer httpRes.Body.Close()
httpBody, err := ioutil.ReadAll(httpRes.Body)
if err != nil {
return hookRes, err
}
// Report an error, if the response has a non-2XX status code
if httpRes.StatusCode < http.StatusOK || httpRes.StatusCode >= http.StatusMultipleChoices {
return hookRes, fmt.Errorf("unexpected response code from hook endpoint (%d): %s", httpRes.StatusCode, string(httpBody))
}
if err = json.Unmarshal(httpBody, &hookRes); err != nil {
return hookRes, fmt.Errorf("failed to parse hook response: %w", err)
}
return hookRes, nil
} }

View File

@ -40,15 +40,16 @@ type Config struct {
// potentially set by proxies when generating an absolute URL in the // potentially set by proxies when generating an absolute URL in the
// response to POST requests. // response to POST requests.
RespectForwardedHeaders bool RespectForwardedHeaders bool
// TODO: Update comments
// PreUploadCreateCallback will be invoked before a new upload is created, if the // PreUploadCreateCallback will be invoked before a new upload is created, if the
// property is supplied. If the callback returns nil, the upload will be created. // property is supplied. If the callback returns nil, the upload will be created.
// Otherwise the HTTP request will be aborted. This can be used to implement // Otherwise the HTTP request will be aborted. This can be used to implement
// validation of upload metadata etc. // validation of upload metadata etc.
PreUploadCreateCallback func(hook HookEvent) error PreUploadCreateCallback func(hook HookEvent) (HTTPResponse, error)
// PreFinishResponseCallback will be invoked after an upload is completed but before // PreFinishResponseCallback will be invoked after an upload is completed but before
// a response is returned to the client. Error responses from the callback will be passed // a response is returned to the client. Error responses from the callback will be passed
// back to the client. This can be used to implement post-processing validation. // back to the client. This can be used to implement post-processing validation.
PreFinishResponseCallback func(hook HookEvent) error PreFinishResponseCallback func(hook HookEvent) (HTTPResponse, error)
} }
func (config *Config) validate() error { func (config *Config) validate() error {

View File

@ -41,6 +41,7 @@ type FileInfo struct {
// more data. Furthermore, a response is sent to notify the client of the // more data. Furthermore, a response is sent to notify the client of the
// interrupting and the upload is terminated (if supported by the data store), // interrupting and the upload is terminated (if supported by the data store),
// so the upload cannot be resumed anymore. // so the upload cannot be resumed anymore.
// TODO: Allow passing in a HTTP Response
func (f FileInfo) StopUpload() { func (f FileInfo) StopUpload() {
if f.stopUpload != nil { if f.stopUpload != nil {
f.stopUpload() f.stopUpload()

View File

@ -31,7 +31,7 @@ func (m Metrics) incRequestsTotal(method string) {
// TODO: Rework to only store error code // TODO: Rework to only store error code
// incErrorsTotal increases the counter for this error atomically by one. // incErrorsTotal increases the counter for this error atomically by one.
func (m Metrics) incErrorsTotal(err HTTPError) { func (m Metrics) incErrorsTotal(err Error) {
ptr := m.ErrorsTotal.retrievePointerFor(err) ptr := m.ErrorsTotal.retrievePointerFor(err)
atomic.AddUint64(ptr, 1) atomic.AddUint64(ptr, 1)
} }
@ -95,10 +95,10 @@ func newErrorsTotalMap() *ErrorsTotalMap {
// retrievePointerFor returns (after creating it if necessary) the pointer to // retrievePointerFor returns (after creating it if necessary) the pointer to
// the counter for the error. // the counter for the error.
func (e *ErrorsTotalMap) retrievePointerFor(err HTTPError) *uint64 { func (e *ErrorsTotalMap) retrievePointerFor(err Error) *uint64 {
serr := ErrorsTotalMapEntry{ serr := ErrorsTotalMapEntry{
ErrorCode: err.ErrorCode(), ErrorCode: err.ErrorCode,
StatusCode: err.StatusCode(), StatusCode: err.HTTPResponse.StatusCode,
} }
e.lock.RLock() e.lock.RLock()

View File

@ -23,69 +23,60 @@ var (
reMimeType = regexp.MustCompile(`^[a-z]+\/[a-z0-9\-\+\.]+$`) reMimeType = regexp.MustCompile(`^[a-z]+\/[a-z0-9\-\+\.]+$`)
) )
// HTTPError represents an error with an additional status code attached // TODO: Move in own file
// which may be used when this error is sent in a HTTP response. // ErrorWithResponse represents an error with an additional HTTP response
// See the net/http package for standardized status codes. // attached, which can hold a status code, body and headers.
type HTTPError interface { type Error struct {
error ErrorCode string
ErrorCode() string Message string
StatusCode() int HTTPResponse HTTPResponse
Body() []byte
} }
type httpError struct { func (e Error) Error() string {
errorCode string return e.ErrorCode + ": " + e.Message
message string
statusCode int
} }
func (err httpError) Error() string { // TODO: Rename comment
return err.errorCode + ": " + err.message // NewError adds the given status code to the provided error and returns
}
func (err httpError) StatusCode() int {
return err.statusCode
}
func (err httpError) ErrorCode() string {
return err.errorCode
}
func (err httpError) Body() []byte {
return []byte(err.Error())
}
// NewHTTPError adds the given status code to the provided error and returns
// the new error instance. The status code may be used in corresponding HTTP // the new error instance. The status code may be used in corresponding HTTP
// responses. See the net/http package for standardized status codes. // responses. See the net/http package for standardized status codes.
func NewHTTPError(errCode string, message string, statusCode int) HTTPError { func NewError(errCode string, message string, statusCode int) Error {
return httpError{errCode, message, statusCode} return Error{
ErrorCode: errCode,
Message: message,
HTTPResponse: HTTPResponse{
StatusCode: statusCode,
Body: []byte(errCode + ": " + message),
},
}
} }
var ( var (
ErrUnsupportedVersion = NewHTTPError("ERR_UNSUPPORTED_VERSION", "missing, invalid or unsupported Tus-Resumable header", http.StatusPreconditionFailed) ErrUnsupportedVersion = NewError("ERR_UNSUPPORTED_VERSION", "missing, invalid or unsupported Tus-Resumable header", http.StatusPreconditionFailed)
ErrMaxSizeExceeded = NewHTTPError("ERR_MAX_SIZE_EXCEEDED", "maximum size exceeded", http.StatusRequestEntityTooLarge) ErrMaxSizeExceeded = NewError("ERR_MAX_SIZE_EXCEEDED", "maximum size exceeded", http.StatusRequestEntityTooLarge)
ErrInvalidContentType = NewHTTPError("ERR_INVALID_CONTENT_TYPE", "missing or invalid Content-Type header", http.StatusBadRequest) ErrInvalidContentType = NewError("ERR_INVALID_CONTENT_TYPE", "missing or invalid Content-Type header", http.StatusBadRequest)
ErrInvalidUploadLength = NewHTTPError("ERR_INVALID_UPLOAD_LENGTH", "missing or invalid Upload-Length header", http.StatusBadRequest) ErrInvalidUploadLength = NewError("ERR_INVALID_UPLOAD_LENGTH", "missing or invalid Upload-Length header", http.StatusBadRequest)
ErrInvalidOffset = NewHTTPError("ERR_INVALID_OFFSET", "missing or invalid Upload-Offset header", http.StatusBadRequest) ErrInvalidOffset = NewError("ERR_INVALID_OFFSET", "missing or invalid Upload-Offset header", http.StatusBadRequest)
ErrNotFound = NewHTTPError("ERR_UPLOAD_NOT_FOUND", "upload not found", http.StatusNotFound) ErrNotFound = NewError("ERR_UPLOAD_NOT_FOUND", "upload not found", http.StatusNotFound)
ErrFileLocked = NewHTTPError("ERR_UPLOAD_LOCKED", "file currently locked", http.StatusLocked) ErrFileLocked = NewError("ERR_UPLOAD_LOCKED", "file currently locked", http.StatusLocked)
ErrMismatchOffset = NewHTTPError("ERR_MISMATCHED_OFFSET", "mismatched offset", http.StatusConflict) ErrMismatchOffset = NewError("ERR_MISMATCHED_OFFSET", "mismatched offset", http.StatusConflict)
ErrSizeExceeded = NewHTTPError("ERR_UPLOAD_SIZE_EXCEEDED", "upload's size exceeded", http.StatusRequestEntityTooLarge) ErrSizeExceeded = NewError("ERR_UPLOAD_SIZE_EXCEEDED", "upload's size exceeded", http.StatusRequestEntityTooLarge)
ErrNotImplemented = NewHTTPError("ERR_NOT_IMPLEMENTED", "feature not implemented", http.StatusNotImplemented) ErrNotImplemented = NewError("ERR_NOT_IMPLEMENTED", "feature not implemented", http.StatusNotImplemented)
ErrUploadNotFinished = NewHTTPError("ERR_UPLOAD_NOT_FINISHED", "one of the partial uploads is not finished", http.StatusBadRequest) ErrUploadNotFinished = NewError("ERR_UPLOAD_NOT_FINISHED", "one of the partial uploads is not finished", http.StatusBadRequest)
ErrInvalidConcat = NewHTTPError("ERR_INVALID_CONCAT", "invalid Upload-Concat header", http.StatusBadRequest) ErrInvalidConcat = NewError("ERR_INVALID_CONCAT", "invalid Upload-Concat header", http.StatusBadRequest)
ErrModifyFinal = NewHTTPError("ERR_MODIFY_FINAL", "modifying a final upload is not allowed", http.StatusForbidden) ErrModifyFinal = NewError("ERR_MODIFY_FINAL", "modifying a final upload is not allowed", http.StatusForbidden)
ErrUploadLengthAndUploadDeferLength = NewHTTPError("ERR_AMBIGUOUS_UPLOAD_LENGTH", "provided both Upload-Length and Upload-Defer-Length", http.StatusBadRequest) ErrUploadLengthAndUploadDeferLength = NewError("ERR_AMBIGUOUS_UPLOAD_LENGTH", "provided both Upload-Length and Upload-Defer-Length", http.StatusBadRequest)
ErrInvalidUploadDeferLength = NewHTTPError("ERR_INVALID_UPLOAD_LENGTH_DEFER", "invalid Upload-Defer-Length header", http.StatusBadRequest) ErrInvalidUploadDeferLength = NewError("ERR_INVALID_UPLOAD_LENGTH_DEFER", "invalid Upload-Defer-Length header", http.StatusBadRequest)
ErrUploadStoppedByServer = NewHTTPError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest) ErrUploadStoppedByServer = NewError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest)
ErrUploadRejectedByServer = NewError("ERR_UPLOAD_REJECTED", "upload creation has been rejected by server", http.StatusBadRequest)
// TODO: These two responses are 500 for backwards compatability. We should discuss // TODO: These two responses are 500 for backwards compatability. We should discuss
// whether it is better to more them to 4XX status codes. // whether it is better to more them to 4XX status codes.
ErrReadTimeout = NewHTTPError("ERR_READ_TIMEOUT", "timeout while reading request body", http.StatusInternalServerError) ErrReadTimeout = NewError("ERR_READ_TIMEOUT", "timeout while reading request body", http.StatusInternalServerError)
ErrConnectionReset = NewHTTPError("ERR_CONNECTION_RESET", "TCP connection reset by peer", http.StatusInternalServerError) ErrConnectionReset = NewError("ERR_CONNECTION_RESET", "TCP connection reset by peer", http.StatusInternalServerError)
) )
// TODO: Move HTTP structs into own file
// HTTPRequest contains basic details of an incoming HTTP request. // HTTPRequest contains basic details of an incoming HTTP request.
type HTTPRequest struct { type HTTPRequest struct {
// Method is the HTTP method, e.g. POST or PATCH // Method is the HTTP method, e.g. POST or PATCH
@ -98,6 +89,15 @@ type HTTPRequest struct {
Header http.Header Header http.Header
} }
type HTTPResponse struct {
// HTTPStatus, HTTPHeaders and HTTPBody control these details of the corresponding
// HTTP response.
// TODO: Currently only works for error responses
StatusCode int
Headers http.Header
Body []byte
}
// 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.
type HookEvent struct { type HookEvent struct {
// Upload contains information about the upload that caused this hook // Upload contains information about the upload that caused this hook
@ -354,7 +354,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
} }
if handler.config.PreUploadCreateCallback != nil { if handler.config.PreUploadCreateCallback != nil {
if err := handler.config.PreUploadCreateCallback(newHookEvent(info, r)); err != nil { if _, err := handler.config.PreUploadCreateCallback(newHookEvent(info, r)); err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
@ -710,7 +710,7 @@ func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, uplo
handler.Metrics.incUploadsFinished() handler.Metrics.incUploadsFinished()
if handler.config.PreFinishResponseCallback != nil { if handler.config.PreFinishResponseCallback != nil {
if err := handler.config.PreFinishResponseCallback(newHookEvent(info, r)); err != nil { if _, err := handler.config.PreFinishResponseCallback(newHookEvent(info, r)); err != nil {
return err return err
} }
} }
@ -950,13 +950,13 @@ func (handler *UnroutedHandler) sendError(w http.ResponseWriter, r *http.Request
// err = nil // err = nil
//} //}
statusErr, ok := err.(HTTPError) detailedErr, ok := err.(Error)
if !ok { if !ok {
handler.log("InternalServerError", "message", err.Error(), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r)) handler.log("InternalServerError", "message", err.Error(), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r))
statusErr = NewHTTPError("ERR_INTERNAL_SERVER_ERROR", err.Error(), http.StatusInternalServerError) detailedErr = NewError("ERR_INTERNAL_SERVER_ERROR", err.Error(), http.StatusInternalServerError)
} }
reason := append(statusErr.Body(), '\n') reason := append(detailedErr.HTTPResponse.Body, '\n')
if r.Method == "HEAD" { if r.Method == "HEAD" {
reason = nil reason = nil
} }
@ -964,12 +964,12 @@ func (handler *UnroutedHandler) sendError(w http.ResponseWriter, r *http.Request
// TODO: Allow JSON response // TODO: Allow JSON response
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(reason))) w.Header().Set("Content-Length", strconv.Itoa(len(reason)))
w.WriteHeader(statusErr.StatusCode()) w.WriteHeader(detailedErr.HTTPResponse.StatusCode)
w.Write(reason) w.Write(reason)
handler.log("ResponseOutgoing", "status", strconv.Itoa(statusErr.StatusCode()), "method", r.Method, "path", r.URL.Path, "error", statusErr.ErrorCode(), "requestId", getRequestId(r)) handler.log("ResponseOutgoing", "status", strconv.Itoa(detailedErr.HTTPResponse.StatusCode), "method", r.Method, "path", r.URL.Path, "error", detailedErr.ErrorCode, "requestId", getRequestId(r))
handler.Metrics.incErrorsTotal(statusErr) handler.Metrics.incErrorsTotal(detailedErr)
} }
// sendResp writes the header to w with the specified status code. // sendResp writes the header to w with the specified status code.

View File

@ -719,7 +719,7 @@ func (upload s3Upload) GetReader(ctx context.Context) (io.Reader, error) {
}) })
if err == nil { if err == nil {
// The multipart upload still exists, which means we cannot download it yet // The multipart upload still exists, which means we cannot download it yet
return nil, handler.NewHTTPError("ERR_INCOMPLETE_UPLOAD", "cannot stream non-finished upload", http.StatusBadRequest) return nil, handler.NewError("ERR_INCOMPLETE_UPLOAD", "cannot stream non-finished upload", http.StatusBadRequest)
} }
if isAwsError(err, "NoSuchUpload") { if isAwsError(err, "NoSuchUpload") {