feat(api): hook default mode configuration

close #74
This commit is contained in:
Nicolas Carlier 2024-07-08 21:48:02 +00:00
parent 7f3dfc472d
commit 4ccbf8408a
4 changed files with 29 additions and 22 deletions

View File

@ -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. 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. 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: The streaming protocol depends on the HTTP request:
- [Server-sent events][sse] is used when `Accept` HTTP header is equal to `text/event-stream`. - [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 [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 [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: - Convert the script exit code to HTTP code as follow:
- 0: `200 OK` - 0: `200 OK`
- Between 1 and 99: `500 Internal Server Error` - Between 1 and 99: `500 Internal Server Error`
@ -143,7 +146,7 @@ error: exit status 118
Streamed output using `Chunked Transfer Coding`: Streamed output using `Chunked Transfer Coding`:
```bash ```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 < HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8 < Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked < Transfer-Encoding: chunked
@ -158,7 +161,7 @@ error: exit status 118
Blocking HTTP request: Blocking HTTP request:
```bash ```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 < HTTP/1.1 418 I m a teapot
< Content-Type: text/plain; charset=utf-8 < Content-Type: text/plain; charset=utf-8
< X-Hook-Id: 9 < X-Hook-Id: 9

View File

@ -19,6 +19,8 @@
# Default extension for hook scripts, default is "sh" # Default extension for hook scripts, default is "sh"
#WHD_HOOK_DEFAULT_EXT=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 # Maximum hook execution time in second, default is 10
#WHD_HOOK_TIMEOUT=10 #WHD_HOOK_TIMEOUT=10
# Scripts location, default is "scripts" # Scripts location, default is "scripts"

View File

@ -22,6 +22,7 @@ import (
var ( var (
defaultTimeout int defaultTimeout int
defaultExt string defaultExt string
defaultMode string
scriptDir string scriptDir string
outputDir string outputDir string
) )
@ -47,6 +48,7 @@ func index(conf *config.Config) http.Handler {
defaultExt = conf.Hook.DefaultExt defaultExt = conf.Hook.DefaultExt
scriptDir = conf.Hook.ScriptsDir scriptDir = conf.Hook.ScriptsDir
outputDir = conf.Hook.LogDir outputDir = conf.Hook.LogDir
defaultMode = conf.Hook.DefaultMode
return http.HandlerFunc(webhookHandler) return http.HandlerFunc(webhookHandler)
} }
@ -65,17 +67,16 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
negociatedContentType := helper.NegotiateContentType(r, supportedContentTypes, "text/plain") negociatedContentType := helper.NegotiateContentType(r, supportedContentTypes, "text/plain")
// Extract streaming method // Extract streaming method
streamingMethod := "none" mode := r.Header.Get("X-Hook-Mode")
transfertEncoding := r.Header.Get("X-Hook-TE") if mode != "buffered" && mode != "chunked" {
if transfertEncoding == "chunked" { mode = defaultMode
streamingMethod = "chunked"
} }
if negociatedContentType == SSEContentType { if negociatedContentType == SSEContentType {
streamingMethod = "sse" mode = "sse"
} }
// Check that streaming is supported // 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) http.Error(w, "streaming not supported", http.StatusInternalServerError)
return return
} }
@ -142,11 +143,11 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
worker.WorkQueue <- job worker.WorkQueue <- job
// Write hook ouput to the response regarding the asked method // Write hook ouput to the response regarding the asked method
if streamingMethod != "none" { if mode != "buffered" {
// Write hook response as Server Sent Event stream // Write hook response as Server Sent Event stream
writeStreamedResponse(w, negociatedContentType, job, streamingMethod) writeStreamedResponse(w, negociatedContentType, job, mode)
} else { } else {
maxBufferLength := atoiFallback(r.Header.Get("X-Hook-MaxOutputLines"), DefaultBufferLength) maxBufferLength := atoiFallback(r.Header.Get("X-Hook-MaxBufferedLines"), DefaultBufferLength)
if maxBufferLength > MaxBufferLength { if maxBufferLength > MaxBufferLength {
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()) writeHeaders(w, negociatedContentType, job.ID())
for { for {
msg, open := <-job.MessageChan msg, open := <-job.MessageChan
@ -163,7 +164,7 @@ func writeStreamedResponse(w http.ResponseWriter, negociatedContentType string,
break break
} }
if method == "sse" { if mode == "sse" {
// Send SSE response // Send SSE response
prefix := "data: " prefix := "data: "
if bytes.HasPrefix(msg, []byte("error:")) { if bytes.HasPrefix(msg, []byte("error:")) {

View File

@ -20,11 +20,12 @@ type Config struct {
// HookConfig store Hook execution configuration // HookConfig store Hook execution configuration
type HookConfig struct { type HookConfig struct {
DefaultExt string `flag:"default-ext" desc:"Default extension for hook scripts" default:"sh"` 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"` DefaultMode string `flag:"default-mode" desc:"Hook default response mode (chuncked,buffered)" default:"chuncked"`
ScriptsDir string `flag:"scripts" desc:"Scripts location" default:"scripts"` Timeout int `flag:"timeout" desc:"Maximum hook execution time in second" default:"10"`
LogDir string `flag:"log-dir" desc:"Hook execution logs location" default:""` ScriptsDir string `flag:"scripts" desc:"Scripts location" default:"scripts"`
Workers int `flag:"workers" desc:"Number of workers to start" default:"2"` 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 // LogConfig store logger configuration