From 4ccbf8408ad1e84bde9454de12db1b10151244cc Mon Sep 17 00:00:00 2001 From: Nicolas Carlier Date: Mon, 8 Jul 2024 21:48:02 +0000 Subject: [PATCH] feat(api): hook default mode configuration close #74 --- README.md | 15 +++++++++------ etc/default/webhookd.env | 2 ++ pkg/api/index.go | 23 ++++++++++++----------- pkg/config/config.go | 11 ++++++----- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 56ba73d..3d0c3fc 100644 --- a/README.md +++ b/README.md @@ -91,19 +91,22 @@ You can omit the script extension. If you do, webhookd will search by default fo You can change the default extension using the `WHD_HOOK_DEFAULT_EXT` environment variable or `-hook-default-ext` parameter. If the script exists, the output will be send to the HTTP response. -Depending on the HTTP request, the HTTP response will be a HTTP `200` code with the script's output in real time (streaming), or the HTTP response will wait until the end of the script's execution and return the output (tuncated) of the script as well as an HTTP code relative to the script's output code : +Depending on the HTTP request, the HTTP response will be a HTTP `200` code with the script's output in real time (streaming), or the HTTP response will wait until the end of the script's execution and return the output (tuncated) of the script as well as an HTTP code relative to the script's output code. The streaming protocol depends on the HTTP request: - [Server-sent events][sse] is used when `Accept` HTTP header is equal to `text/event-stream`. -- [Chunked Transfer Coding][chunked] is used when `X-Hook-TE` HTTP header is equal to `chunked`. +- [Chunked Transfer Coding][chunked] is used when `X-Hook-Mode` HTTP header is equal to `chunked`. +It's the default mode. +You can change the default mode using the `WHD_HOOK_DEFAULT_MODE` environment variable or `-hook-default-mode` parameter. [sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events [chunked]: https://datatracker.ietf.org/doc/html/rfc2616#section-3.6.1 -If no streaming protocol is asked, the HTTP reponse block until the script is over: +If no streaming protocol is needed, yous must set `X-Hook-Mode` HTTP header to `buffered`. +The HTTP reponse will block until the script is over: -- Sends script output limited to the last 100 lines. You can modify this limit via the HTTP header `X-Hook-MaxOutputLines`. +- Sends script output limited to the last 100 lines. You can modify this limit via the HTTP header `X-Hook-MaxBufferedLines`. - Convert the script exit code to HTTP code as follow: - 0: `200 OK` - Between 1 and 99: `500 Internal Server Error` @@ -143,7 +146,7 @@ error: exit status 118 Streamed output using `Chunked Transfer Coding`: ```bash -$ curl -v -XPOST --header "X-Hook-TE: chunked" http://localhost:8080/foo/bar +$ curl -v -XPOST --header "X-Hook-Mode: chunked" http://localhost:8080/foo/bar < HTTP/1.1 200 OK < Content-Type: text/plain; charset=utf-8 < Transfer-Encoding: chunked @@ -158,7 +161,7 @@ error: exit status 118 Blocking HTTP request: ```bash -$ curl -v -XPOST http://localhost:8080/foo/bar +$ curl -v -XPOST --header "X-Hook-Mode: buffered" http://localhost:8080/foo/bar < HTTP/1.1 418 I m a teapot < Content-Type: text/plain; charset=utf-8 < X-Hook-Id: 9 diff --git a/etc/default/webhookd.env b/etc/default/webhookd.env index 3dde73c..77d03d6 100644 --- a/etc/default/webhookd.env +++ b/etc/default/webhookd.env @@ -19,6 +19,8 @@ # Default extension for hook scripts, default is "sh" #WHD_HOOK_DEFAULT_EXT=sh +# Default hook HTTP response mode (chunked or buffered), default is "chunked" +#WHD_HOOK_DEFAULT_MODE=chunked # Maximum hook execution time in second, default is 10 #WHD_HOOK_TIMEOUT=10 # Scripts location, default is "scripts" diff --git a/pkg/api/index.go b/pkg/api/index.go index c9e811f..48bfe84 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -22,6 +22,7 @@ import ( var ( defaultTimeout int defaultExt string + defaultMode string scriptDir string outputDir string ) @@ -47,6 +48,7 @@ func index(conf *config.Config) http.Handler { defaultExt = conf.Hook.DefaultExt scriptDir = conf.Hook.ScriptsDir outputDir = conf.Hook.LogDir + defaultMode = conf.Hook.DefaultMode return http.HandlerFunc(webhookHandler) } @@ -65,17 +67,16 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) { negociatedContentType := helper.NegotiateContentType(r, supportedContentTypes, "text/plain") // Extract streaming method - streamingMethod := "none" - transfertEncoding := r.Header.Get("X-Hook-TE") - if transfertEncoding == "chunked" { - streamingMethod = "chunked" + mode := r.Header.Get("X-Hook-Mode") + if mode != "buffered" && mode != "chunked" { + mode = defaultMode } if negociatedContentType == SSEContentType { - streamingMethod = "sse" + mode = "sse" } // Check that streaming is supported - if _, ok := w.(http.Flusher); !ok && streamingMethod != "none" { + if _, ok := w.(http.Flusher); !ok && mode != "buffered" { http.Error(w, "streaming not supported", http.StatusInternalServerError) return } @@ -142,11 +143,11 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) { worker.WorkQueue <- job // Write hook ouput to the response regarding the asked method - if streamingMethod != "none" { + if mode != "buffered" { // Write hook response as Server Sent Event stream - writeStreamedResponse(w, negociatedContentType, job, streamingMethod) + writeStreamedResponse(w, negociatedContentType, job, mode) } else { - maxBufferLength := atoiFallback(r.Header.Get("X-Hook-MaxOutputLines"), DefaultBufferLength) + maxBufferLength := atoiFallback(r.Header.Get("X-Hook-MaxBufferedLines"), DefaultBufferLength) if maxBufferLength > MaxBufferLength { maxBufferLength = MaxBufferLength } @@ -155,7 +156,7 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) { } } -func writeStreamedResponse(w http.ResponseWriter, negociatedContentType string, job *hook.Job, method string) { +func writeStreamedResponse(w http.ResponseWriter, negociatedContentType string, job *hook.Job, mode string) { writeHeaders(w, negociatedContentType, job.ID()) for { msg, open := <-job.MessageChan @@ -163,7 +164,7 @@ func writeStreamedResponse(w http.ResponseWriter, negociatedContentType string, break } - if method == "sse" { + if mode == "sse" { // Send SSE response prefix := "data: " if bytes.HasPrefix(msg, []byte("error:")) { diff --git a/pkg/config/config.go b/pkg/config/config.go index 7094197..9a94884 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -20,11 +20,12 @@ type Config struct { // HookConfig store Hook execution configuration type HookConfig struct { - DefaultExt string `flag:"default-ext" desc:"Default extension for hook scripts" default:"sh"` - Timeout int `flag:"timeout" desc:"Maximum hook execution time in second" default:"10"` - ScriptsDir string `flag:"scripts" desc:"Scripts location" default:"scripts"` - LogDir string `flag:"log-dir" desc:"Hook execution logs location" default:""` - Workers int `flag:"workers" desc:"Number of workers to start" default:"2"` + DefaultExt string `flag:"default-ext" desc:"Default extension for hook scripts" default:"sh"` + DefaultMode string `flag:"default-mode" desc:"Hook default response mode (chuncked,buffered)" default:"chuncked"` + Timeout int `flag:"timeout" desc:"Maximum hook execution time in second" default:"10"` + ScriptsDir string `flag:"scripts" desc:"Scripts location" default:"scripts"` + LogDir string `flag:"log-dir" desc:"Hook execution logs location" default:""` + Workers int `flag:"workers" desc:"Number of workers to start" default:"2"` } // LogConfig store logger configuration