webhook

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2025 License: Apache-2.0 Imports: 18 Imported by: 0

README

Golang Webhook Library

This library aims to provide an easy to use method for using webhooks in your applications. It provides webhook methods for 'POST', 'PUT', 'PATCH', 'GET', 'DELETE'

It supports using JSON body payloads for 'POST', 'PUT' and 'PATCH' and URL query parameters for 'GET' and 'DELETE'

You can set custom headers for webhooks and set authentication parameters suitable for authentication with remote endpoints. The supported authentication types are 'none', 'basic', 'bearer' and 'token'

Summary

  • POST/PUT/PATCH use JSON body (data is required).
  • GET/DELETE use URL query parameters. If you also provide data, it will be flattened into query params automatically (no HTTP body is sent for these verbs).
  • You can attach custom headers and choose auth: none, basic, bearer, or token.
  • Security: The library enforces per-hook auth: it overwrites any global Authorization/token headers for that hook and removes them when authType is none.

Quick start (HTTP POST with JSON body)

package main

import (
	"crypto/tls"
	"encoding/json"
	"log"
	"time"

	"github.com/handletec/webhook"
)

type WebHookRequest struct {
	Action    string `json:"action"`
	Timestamp string `json:"timestamp"`
}

func main() {
	// Payload (raw JSON bytes)
	body, err := json.Marshal(&WebHookRequest{
		Action:    "create",
		Timestamp: time.Now().Format(time.RFC3339),
	})
	if err != nil {
		log.Fatal(err)
	}

	// Create registry and add an endpoint (no auth)
	whs := webhook.NewWebHooks()
	if err := whs.Add(webhook.MethodPost, "https://echo.app.handletec.my", webhook.WithNoAuth()); err != nil {
		log.Fatal(err)
	}

	// TLS for testing only; do NOT use InsecureSkipVerify in production
	tlsCfg := &tls.Config{InsecureSkipVerify: true}

	// Optional headers
	h := webhook.NewHeaders()
	h.SetUserAgent("my-agent/v1-post")


	// Send to all registered hooks
	if err := whs.Broadcast(tlsCfg, webhook.WithData(body), webhook.WithHeaders(h)); err != nil {
		log.Fatal(err)
	}
}

You can also let the library marshal for you:

_ = whs.Broadcast(tlsCfg, webhook.WithJSON(WebHookRequest{Action: "create", Timestamp: time.Now().Format(time.RFC3339)}))

Authentication

Basic

Authorization: Basic <base64(username:password)>

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my",
    webhook.WithBasicAuth("hello", "world"),
)

Bearer

Authorization: Bearer <token>

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my",
    webhook.WithBearerToken("mysupersecrettoken"),
)

Token (custom header)

X-Api-Key: <value> (or any header name you choose)

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my",
    webhook.WithToken("X-Api-Key", "secret"),
)

If you load hooks from JSON/YAML and want to bind secrets at runtime:

// Bind secrets at runtime (kept out of config files):
_ = whs.ApplyAuth(func(h *webhook.WebHook) error {
    switch h.AuthType {
    case webhook.AuthTypeBasic:
        user, pass := lookupBasic(h.Address)
        h.SetBasicAuth(user, pass)
    case webhook.AuthTypeBearer:
        token := lookupBearer(h.Address)
        h.SetBearerToken(token)
    case webhook.AuthTypeToken:
        value := lookupToken(h.Address)
        h.SetCustomToken(h.AuthHeaderName, value)
    case webhook.AuthTypeNone:
        // nothing
    }
    return nil
})

Headers

hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1")
hdrs.Set("X-Trace-Id", "12345")
hdrs.Add("X-Multi", "a")
hdrs.Add("X-Multi", "b")

if v, ok := hdrs.Get("X-Trace-Id"); ok {
    fmt.Println("trace:", v)
}
hdrs.Del("X-Multi")

Headers are cloned per request for safe concurrent fan-out.


GET and DELETE

GET and DELETE use the query string. If you also supply data via WithData/WithJSON, it will be flattened into query parameters (best effort: primitives → k=v, arrays → repeated keys, nested objects → JSON string).

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodGet, "https://echo.app.handletec.my", webhook.WithNoAuth())

q := webhook.NewQuery()
q.Add("action", "create")
q.Add("timestamp", time.Now().Format(time.RFC3339))

tlsCfg := &tls.Config{InsecureSkipVerify: true}
hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1-get")


// You may provide both query AND data; data will be merged into the query for GET/DELETE.
_ = whs.Broadcast(tlsCfg, webhook.WithQuery(q), webhook.WithJSON(map[string]any{"extra": "value"}), webhook.WithHeaders(hdrs))

Configuration (YAML / JSON)

Define your hooks in config, load them, then bind secrets at runtime.

# example.yaml
webhooks:
  POST:
    - address: https://api.example.com/hookA         # shorthand item
    - address: https://api.example.com/hookB
      authType: bearer
      enabled: true
    - address: https://api.example.com/hookC
      authType: token
      authHeaderName: X-Api-Key
      enabled: true
  GET:
    - address: https://api.example.com/audit
      authType: none
      enabled: true

After unmarshalling into webhook.WebHooks:

err := webhooks.ApplyAuth(func(h *webhook.WebHook) error {
	  switch h.AuthType {
	  case webhook.AuthTypeBasic:
		  user, pass := lookupBasic(h.Address)
		  h.SetBasicAuth(user, pass)
	  case webhook.AuthTypeBearer:
		  token := lookupBearer(h.Address)
		  h.SetBearerToken(token)
	  case webhook.AuthTypeToken:
		  value := lookupToken(h.Address)
		  h.SetCustomToken(h.AuthHeaderName, value)
	  }
	  return nil
})
if err != nil {
	  log.Fatal(err)
}

Adding and removing hooks

whs := webhook.NewWebHooks()

// Add (dedupes by address per method)
_ = whs.Add(webhook.MethodPost, "https://api.example.com/hook", webhook.WithNoAuth())

// Remove
_ = whs.Remove(webhook.MethodPost, "https://api.example.com/hook")

Request options and rules

Use options to describe what you’re sending:

  • WithData([]byte) — raw JSON body (POST/PUT/PATCH) or data to be merged into query (GET/DELETE)
  • WithJSON(any) — convenience: json.Marshal then treated like WithData
  • WithQuery(webhook.Query) — additional query params (always preserved)
  • WithHeaders(webhook.Headers) — extra headers

Rules enforced by the library:

  • POST/PUT/PATCH require non-empty data.
  • GET/DELETE never send an HTTP body; if data is supplied, it is flattened into the query.
  • When a body is sent (POST/PUT/PATCH), the library sets
    Content-Type: application/json; charset=utf-8 and Accept: application/json.

Note: unlike earlier versions, it is valid to pass both data and query in the same call.


Concurrency and errors

  • Send/Broadcast fan out concurrently using a bounded worker pool (based on GOMAXPROCS).
  • Errors from multiple targets are aggregated and returned (Go 1.20+ errors.Join).

TLS and timeouts

Pass a custom *tls.Config (custom roots, mTLS, etc.). Each request uses a 15s timeout.

tlsCfg := &tls.Config{InsecureSkipVerify: true} // testing only
_ = whs.Broadcast(tlsCfg, webhook.WithData(body))

Do not enable InsecureSkipVerify in production.


Examples mirrored from tests

No auth, POST with JSON body

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my", webhook.WithNoAuth())

tlsCfg := &tls.Config{InsecureSkipVerify: true}
hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1-noauth-post")


_ = whs.Broadcast(tlsCfg, webhook.WithData(b), webhook.WithHeaders(hdrs))

Basic auth, POST with JSON body

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my",
    webhook.WithBasicAuth("hello", "world"),
)

tlsCfg := &tls.Config{InsecureSkipVerify: true}
hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1-basicauth-post")


_ = whs.Broadcast(tlsCfg, webhook.WithData(b), webhook.WithHeaders(hdrs))

Bearer auth, POST with JSON body

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my",
    webhook.WithBearerToken("mysupersecrettoken"),
)

tlsCfg := &tls.Config{InsecureSkipVerify: true}
hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1-bearer-post")


_ = whs.Broadcast(tlsCfg, webhook.WithData(b), webhook.WithHeaders(hdrs))

Custom token header, POST with JSON body

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodPost, "https://echo.app.handletec.my",
    webhook.WithToken("X-Api-Key", "secret"),
)

tlsCfg := &tls.Config{InsecureSkipVerify: true}
hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1-token-post")


_ = whs.Broadcast(tlsCfg, webhook.WithData(b), webhook.WithHeaders(hdrs))

GET with query parameters (plus extra data flattened)

whs := webhook.NewWebHooks()
_ = whs.Add(webhook.MethodGet, "https://echo.app.handletec.my", webhook.WithNoAuth())

tlsCfg := &tls.Config{InsecureSkipVerify: true}
hdrs := webhook.NewHeaders()
hdrs.SetUserAgent("my-agent/v1-noauth-get")

// data is flattened into the query for GET
_ = whs.Broadcast(tlsCfg, webhook.WithQuery(q), webhook.WithJSON(map[string]any{"extra": "value"}), webhook.WithHeaders(hdrs))

API overview

Construction:

whs := webhook.NewWebHooks()
err := whs.Add(method, address, opt) // opt: WithNoAuth | WithBasicAuth | WithBearerToken | WithToken
err := whs.Remove(method, address)

Sending:

// single method set
err := whs.Send(webhook.MethodPost, tlsCfg, webhook.WithData(body), webhook.WithHeaders(h))

// all methods
err := whs.Broadcast(tlsCfg, webhook.WithQuery(q), webhook.WithHeaders(h))

Request options:

webhook.WithData([]byte)          // raw JSON body or data to flatten
webhook.WithJSON(any)             // marshal then treated like WithData
webhook.WithQuery(webhook.Query)  // url.Values
webhook.WithHeaders(webhook.Headers)

Auth options at construction:

webhook.WithNoAuth()
webhook.WithBasicAuth(username, password)
webhook.WithBearerToken(token)
webhook.WithToken(headerName, value) // e.g., "X-Api-Key", "secret"

Behavior and defaults

  • POST, PUT, PATCH require a non-nil body. The library sets Content-Type: application/json; charset=utf-8 and Accept: application/json.
  • GET, DELETE ignore the body and rely on query parameters (if you pass WithData/WithJSON, it is flattened into the query).
  • Headers passed by the caller are cloned per request.
  • Security: Per-hook auth overwrites any global auth headers; hooks with authType: none strip auth headers to prevent leakage.
  • Sends fan out concurrently; multiple errors are aggregated and returned.
  • For testing you may use InsecureSkipVerify in a custom tls.Config. Don’t use it in production.

Documentation

Overview

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Copyright © 2025 Vicknesh Suppramaniam <vicknesh@handletec.my>

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Index

Constants

View Source
const (
	// AuthTypeNone - no authentication required
	AuthTypeNone = iota + 1
	// AuthTypeBasic - username password combination
	AuthTypeBasic
	// AuthTypeBearer - bearer token authentication
	AuthTypeBearer
	// AuthType - uses custom token authentication
	AuthTypeToken
)
View Source
const (
	// MethodGet - HTTP GET Method
	MethodGet = iota + 1
	// Method - HTTP POST Method
	MethodPost
	// Method - HTTP PUT Method
	MethodPut
	// Method - HTTP PATCH Method
	MethodPatch
	// Method - HTTP DELETE Method
	MethodDelete
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthBinderFunc

type AuthBinderFunc func(h *WebHook) error

AuthBinderFunc is invoked once per hook; set creds via wh.SetBasicAuth / wh.SetBearerToken / wh.SetCustomToken as needed.

type AuthType

type AuthType uint8

func (*AuthType) MarshalJSON

func (at *AuthType) MarshalJSON() (data []byte, err error)

func (AuthType) MarshalYAML

func (at AuthType) MarshalYAML() (any, error)

func (AuthType) String

func (at AuthType) String() (str string)

func (*AuthType) UnmarshalJSON

func (at *AuthType) UnmarshalJSON(data []byte) (err error)

func (*AuthType) UnmarshalYAML

func (at *AuthType) UnmarshalYAML(n *yaml.Node) error

type Headers

type Headers httpclient.Headers

func NewHeaders

func NewHeaders() (h Headers)

NewHeaders - initialize headers with default user-agent

func (*Headers) Add

func (h *Headers) Add(name, value string)

Add appends a header without removing existing ones.

func (Headers) Clone

func (h Headers) Clone() Headers

Clone returns a shallow copy of the slice (safe to pass to goroutines).

func (*Headers) Del

func (h *Headers) Del(name string)

Del removes all instances of the header name (case-insensitive).

func (Headers) Get

func (h Headers) Get(name string) (string, bool)

Get returns the first value for name (case-insensitive) and true if found.

func (*Headers) Set

func (h *Headers) Set(name, value string)

Set sets/overwrites the header (keeps only one entry for that name).

func (*Headers) SetUserAgent

func (h *Headers) SetUserAgent(ua string)

Convenience

type HookOpt

type HookOpt func(*WebHook) error

func WithBasicAuth

func WithBasicAuth(username, password string) HookOpt

WithBasicAuth - Authorization: Basic <base64(username:password)>

func WithBearerToken

func WithBearerToken(token string) HookOpt

WithBearerToken - Authorization: Bearer <token> (JWT or opaque)

func WithNoAuth

func WithNoAuth() HookOpt

WithNoAuth - no authentication needed for remote endpoint

func WithToken

func WithToken(headerName, value string) HookOpt

WithToken - Custom header token, e.g. X-Api-Key: <value>

type Method

type Method uint8

func (*Method) MarshalJSON

func (method *Method) MarshalJSON() (data []byte, err error)

func (Method) MarshalYAML

func (m Method) MarshalYAML() (any, error)

func (Method) String

func (method Method) String() (str string)

func (*Method) UnmarshalJSON

func (method *Method) UnmarshalJSON(data []byte) (err error)

func (*Method) UnmarshalYAML

func (m *Method) UnmarshalYAML(n *yaml.Node) error

type Query

type Query = url.Values

func NewQuery

func NewQuery() Query

func NewQueryPairs

func NewQueryPairs(pairs ...string) Query

type ReqOpt

type ReqOpt func(*reqSpec) error

func WithData

func WithData(b []byte) ReqOpt

WithData - body payload

func WithHeaders

func WithHeaders(h Headers) ReqOpt

WithHeaders - headers for the HTTP

func WithJSON

func WithJSON(v any) ReqOpt

func WithQuery

func WithQuery(q Query) ReqOpt

WithQuery - query parameters

type WebHook

type WebHook struct {
	Enabled        bool     `yaml:"enabled" json:"enabled"`
	Method         Method   `yaml:"method" json:"method"`
	Address        string   `yaml:"address" json:"address"`
	AuthType       AuthType `yaml:"authType" json:"authType"`
	AuthHeaderName string   `yaml:"authHeaderName,omitempty" json:"authHeaderName,omitempty"`

	// for auth type basic
	Username string `yaml:"username,omitempty" json:"username,omitempty"`
	Password string `yaml:"password,omitempty" json:"password,omitempty"`

	// for auth type token
	Token string `yaml:"token,omitempty" json:"token,omitempty"`
	// contains filtered or unexported fields
}

WebHook - remote service to notify based

func NewWebHook

func NewWebHook(method Method, address string, opt HookOpt) (*WebHook, error)

NewWebHook - creates new webhook

func (*WebHook) MarshalYAML

func (wh *WebHook) MarshalYAML() (any, error)

func (*WebHook) Send

func (wh *WebHook) Send(tlsConfig *tls.Config, body []byte, query Query, headers Headers) (err error)

Send - sends the request to the endpoint, no response returned - fire and forget

func (*WebHook) SetBasicAuth

func (wh *WebHook) SetBasicAuth(username, password string)

SetBasicAuth - Authorization: Basic <base64(username:password)>

func (*WebHook) SetBearerToken

func (wh *WebHook) SetBearerToken(token string)

SetBearerToken - Authorization: Bearer <token> (JWT or opaque token)

func (*WebHook) SetCustomToken

func (wh *WebHook) SetCustomToken(headerName, value string)

SetCustomToken - sets custom headwe with value e.g., X-Api-Key: <value>

func (*WebHook) SetHeader

func (wh *WebHook) SetHeader(name, value string)

SetHeader sets or overwrites a persistent default header for this WebHook instance. These headers are merged into every Send() call, unless explicitly overridden by WithHeaders.

func (*WebHook) String

func (wh *WebHook) String() (str string)

func (*WebHook) UnmarshalYAML

func (wh *WebHook) UnmarshalYAML(n *yaml.Node) error

UnmarshalYAML implements yaml.Unmarshaler for WebHook. Pointer receiver avoids copying the embedded RWMutex.

type WebHooks

type WebHooks map[Method][]*WebHook

WebHooks - collection of `WebHook`

func NewWebHooks

func NewWebHooks() (whs WebHooks)

NewWebHooks - create new instance of webhooks to store multiple webhook for different HTTP methods

func (WebHooks) Add

func (whs WebHooks) Add(method Method, address string, opt HookOpt) error

Add - adds a new webhook

func (*WebHooks) ApplyAuth

func (whs *WebHooks) ApplyAuth(binder AuthBinderFunc) error

ApplyAuth - applies credentials to every hook using the provided binder.

func (WebHooks) Broadcast

func (whs WebHooks) Broadcast(tlsCfg *tls.Config, opts ...ReqOpt) error

Broadcast - sends the request to all addresses across all methods (concurrently)

func (WebHooks) MarshalYAML

func (whs WebHooks) MarshalYAML() (any, error)

func (WebHooks) Remove

func (whs WebHooks) Remove(method Method, address string) (err error)

Remove - Removes an existing webhook

func (WebHooks) Send

func (whs WebHooks) Send(method Method, tlsCfg *tls.Config, opts ...ReqOpt) error

Send - sends the request to all addresses for a given method (concurrently)

func (*WebHooks) String

func (whs *WebHooks) String() (str string)

func (*WebHooks) UnmarshalYAML

func (whs *WebHooks) UnmarshalYAML(n *yaml.Node) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL