網頁

2022/4/25

Golang Webhooks簡單實作

Go語言實作簡單的webhooks練習。


範例環境:

  • Go 1.18


事前要求

參考「Golang 建立網頁伺服器 Web Server」建立一個簡單的Go網路應用程式。

注意本範例server的webhooks及註冊webhooks的client的程式皆在同一個Go專案,分別以目錄serverclient區隔。


Server Webhhooks

Webhooks需要:

  • 提供client註冊/訂閱webhooks的API endpoint
  • 儲存client註冊webhooks的URL。
  • 提供client設計接收webhooks發送請求的資料格式

在專案根目錄建立server目錄用於存放server端的程式碼。


建立server/route.go如下。

/regsiter轉交WebhooksHandler.Register()
/greeting轉交WebhooksHandler.Greeting()

server/route.go

package server

import "net/http"

func Route(wh *WebhooksHandler) {
    http.HandleFunc("/register", wh.Register)
    http.HandleFunc("/greeting", wh.Greeting)
}

建立server/handler.go如下。

WebhooksHandler.Register()case http.MethodPostPOST|/register)處理client註冊webhooks的請求URL。註冊時提供名稱及URL的JSON request body,由WebhooksRegisterRequest接收後轉交WebhooksService.Save()儲存起來,並返回webhooks的請求格式文件events.yaml給client。

WebhooksHandler.Register()case http.MethodGetGET|/register)調用WebhooksService.GetRegisteredUrls()取得client註冊webhooks的URL清單。

WebhooksHandler.Greeting()case http.MethodGetGET|greeting)為server的一簡單服務,執行時會以http.Post()發送請求給client註冊的URL,發送的請求JSON格式即為client註冊webhooks成功時返回的yaml文件。

server/handler.go

package server

import (
    "bytes"
    _ "embed"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type WebhooksHandler struct {
    WebhooksService *WebhooksService
}

func NewWebhooksHandler(ws *WebhooksService) *WebhooksHandler {
    return &WebhooksHandler{
        WebhooksService: ws,
    }
}

//go:embed resources/events.yaml
var events string

func (wh *WebhooksHandler) Register(rw http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodPost:
        var req WebhooksRegisterRequest
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&req)
        if err != nil {
            panic(err)
        }
        wh.WebhooksService.Save(req)
        rw.Write([]byte(events))
    case http.MethodGet:
        rw.Header().Set("Content-Type", "application/json")
        json.NewEncoder(rw).Encode(wh.WebhooksService.GetRegisteredUrls())
    default:
        http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func (wh *WebhooksHandler) Greeting(rw http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        fmt.Println("Good day")
        dtos := wh.WebhooksService.GetRegisteredDtos()
        if len(dtos) == 0 {
            fmt.Println("webhooks has no registered urls")
            return
        }

        for _, dto := range wh.WebhooksService.GetRegisteredDtos() {
            data := fmt.Sprintf("{\"name\": \"%s\"}", dto.Name)
            resp, err := http.Post(dto.URL, "application/json", bytes.NewBuffer([]byte(data)))
            if err != nil {
                fmt.Printf("send event failed")
            }

            b, err := io.ReadAll(resp.Body)
            msg := string(b)
            if msg == "success" {
                fmt.Println("send event success")
            }
        }
    default:
        http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

建立server/request.go如下。WebhooksRegisterRequest用於接收POST|/register client註冊webhooks的JSON資料。

sever/request.go

package server

type WebhooksRegisterRequest struct {
    Name string
    URL  string
}

server/resources/events.yaml為client呼叫POST|/register註冊webhooks成功時返回的webhooks發送請求格式文件。格式為OpenAPI,可以Swagger Editor開啟。

server/reources/events.yaml

openapi: 3.0.0
info:
  contact: 
    name: "菜鳥工程師肉豬-Golang Webhooks簡單實作"
    url: "https://matthung0807.blogspot.com/2022/04/go-webhooks-simple-impl.html"
  title: "Webhooks Events API"
  version: "1.0.0"
paths:
  "/hello":
    post:
      description: Hello
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Hello"
        required: true
      responses:
        "200":
          description: Success
          content:
            text/plain:
              schema:
                type: string
                example: success
      tags:
        - Hello
components:
  schemas:
    Hello:
      type: object
      required:
        - name
      properties:
        name:
          type: string

建立server/service.go如下。

WebhooksService.Save()負責把WebhooksHandler.Register() POST送來的client註冊請求轉為WebhooksRegisterDto儲存在模擬資料庫的registered map變數中。

GetRegisteredDtos()方法從registered取得client註冊webhooks的資料用以在觸發webhooks時將請求送給client註冊的URL。

GetRegisteredUrls()方法取得client註冊webhooks的url清單。

server/service.go

package server

import (
    "sync"
    "time"
)

type WebhooksService struct {
    registered map[string]WebhooksRegisterDto
    rwmu       sync.RWMutex
}

func NewWebhooksService() *WebhooksService {
    return &WebhooksService{
        registered: make(map[string]WebhooksRegisterDto),
        rwmu:       sync.RWMutex{},
    }
}

func (ws *WebhooksService) Save(req WebhooksRegisterRequest) {
    ws.rwmu.Lock()
    defer ws.rwmu.Unlock()
    ws.registered[req.URL] = WebhooksRegisterDto{
        Name:      req.Name,
        URL:       req.URL,
        CreatedAt: time.Now(),
    }
}

func (ws *WebhooksService) GetRegisteredDtos() []WebhooksRegisterDto {
    ws.rwmu.RLock()
    defer ws.rwmu.RUnlock()
    dtos := make([]WebhooksRegisterDto, 0)
    for _, v := range ws.registered {
        dtos = append(dtos, v)
    }
    return dtos
}

func (ws *WebhooksService) GetRegisteredUrls() []string {
    urls := make([]string, 0)
    for _, dto := range ws.GetRegisteredDtos() {
        urls = append(urls, dto.URL)
    }
    return urls
}

建立server/dto.go如下。WebhooksRegisterDto為server儲存client註冊webhooks資料的資料物件。

server/dto.go

package server

import "time"

type WebhooksRegisterDto struct {
    Name      string
    URL       string
    CreatedAt time.Time
}


Client

在專案根目錄建立client目錄用於存放client端的程式碼。


建立client/route.go如下。/hello轉交HelloHandler()

client/route.go

package client

import "net/http"

func Route() {
    http.HandleFunc("/hello", HelloHandler)
}

建立client/handler.go如下。

HelloHandler()case http.MethodPostPOST|/hello)為向server註冊的URL。當server的webhooks觸發時會呼叫此endpoints。接收的請求資料HelloRequest格式為client註冊webhooks成功時返回的yaml文件。

client/handler.go

package client

import (
    "encoding/json"
    "fmt"
    "net/http"
)

func HelloHandler(rw http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodPost:
        var req HelloRequest
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&req)
        if err != nil {
            panic(err)
        }
        fmt.Printf("Hello %s\n", req.Name)
        rw.Write([]byte("success"))
    default:
        http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

建立client/request.go如下。HelloRequest為接收webhooks請求的資料物件。

client/request.go

package client

type HelloRequest struct {
    Name string
}


主程式main.go啟動server及client。

main.go

package main

import (
    "net/http"

    c "abc.com/demo/client"
    s "abc.com/demo/server"
)

func main() {
    server()
    client()
    http.ListenAndServe(":8080", nil)
}

func server() {
    s.Route(s.NewWebhooksHandler(s.NewWebhooksService()))
}

func client() {
    c.Route()
}

github


測試

完成以上server webhooks及client後,接著以client的角度測試webhooks的效果。

啟動專案。使用curl呼叫server的POST|/register註冊webhooks並以JSON提供註冊的名稱及URL http://localhost:8080/hello。成功後返回webhooks的請求格式文件。

$ curl -X POST "http://localhost:8080/register" \
> -H 'content-type: application/json' \
> -d '{"url": "http://localhost:8080/hello", "name": "john"}'
openapi: 3.0.0
info:
  contact:
    name: "菜鳥工程師肉豬-Go Webhooks簡單實作"
    url: "https://matthung0807.blogspot.com/2022/04/go-webhooks-simple-impl.html"
  title: "Webhooks Events API"
  version: "1.0.0"
paths:
  "/hello":
    post:
      description: Hello
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Hello"
        required: true
      responses:
        "200":
          description: Success
          content:
            text/plain:
              schema:
                type: string
                example: success
      tags:
        - Hello
components:
  schemas:
    Hello:
      type: object
      required:
        - name
      properties:
        name:
          type: string

呼叫GET/register即返回剛註冊的http://localhost:8080/hello

$ curl -X GET "http://localhost:8080/register"
["http://localhost:8080/hello"]

呼叫GET/greeting觸發webhooks。

$ curl -X GET "http://localhost:8080/greeting"

此時在console會印出以下。其中Hello john為client GET/hello被server webhooks觸發的結果。

Good day
Hello john
send event success

觸發webhooks循序圖。

               ┌────────┐              ┌────────┐
               │ client │              │ Server │
               └────┬───┘              └────┬───┘
                    │                       │
                                             
                    │     GET|/greeting     │
                   ┌┼─────────────────────►┌┤
                   ││                      ││
                   ││                      ││
                   ││                      │┼─print "Good day"
                   ││                      ││
                   ││                      ││
                   ││                      │┼─retrive webhooks registered urls
                   ││                      ││
                   ││      POST|/hello     ││
                   │┤◄─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┼┼─send webooks event request
                   ││                      ││
Print "Hello john"─┼│                      ││
                   ││                      ││
                   └┤                      └┼─print "send event success"
                    │                       │
                                              
                    │                       │
                    ▼                       ▼

以上即為webhooks的簡單範例,但通常註冊時需要提供token做為client驗證。


沒有留言:

張貼留言