Forwarding Webhooks to Customer Systems
Overview
Some customers run their own systems that also want to receive LINE webhooks, but LINE only allows one webhook URL per OA. This feature solves that: our system is the single destination LINE knows about, and it copies the original request in full and fires it at the customer's endpoint.
The key benefit is that the customer can still verify x-line-signature exactly as if they had
received the request directly from LINE, because we pass along the raw bytes together with the
original signature header (see
Forwarding the Raw Body and Signature).
Forwarding is doubly fire-and-forget: the main pipeline does not wait for it, and the forward itself spawns its own goroutine. A dead customer endpoint therefore has no effect on our normal flow.
Business Flow
Path 1 — general forwarding (all events)
ProcessLinereadswebhook_configand finds a non-emptyforwardWebhookUrl.- It calls
forwardWebhookwith the URL, headers, body, and raw bytes, without waiting for a result. - The main pipeline continues as normal — it still enters mbox routing or publishes to the queue. Forwarding does not replace normal processing; it runs in parallel with it.
Path 2 — forwarding while in agent mode
- When the user is in agent mode and the message is not an exit keyword, the message is
forwarded to
mboxLineWebhookUrlinstead, so the mbox system can display it on the agent's screen (see Handoff to Human Agents).
Request details
- The URL is parsed first. If parsing fails, the service logs the warning
Forward webhook failed: invalid urland abandons the send. - A goroutine issues a
POSTto the target URL usingcontext.WithTimeoutset to 5 seconds. - The body sent is
BodyRaw, the original bytes. Only when no raw body exists does it marshal from the map. - The only headers sent are
content-type: application/jsonandx-line-signature(when present on the inbound request). No other headers are forwarded — no user-agent, no x-forwarded-for. req.Hostis set to the target URL's host.- The response is drained with
io.Copy(io.Discard)and closed, and the status code is ignored. If the destination returns 500 there is no log, no retry, and no dead letter queue. - Network-level errors produce a warning log and nothing more.
:::warning Known limitations There is no retry, no circuit breaker, and no metrics collection. If a customer's system goes down, events during that window are silently lost and can only be traced afterwards through warning logs. :::
Key Files & Functions
| File | Function |
|---|---|
internal/line/service.go | Service.forwardWebhook(rawURL, headers, body, raw) — the core implementation |
internal/line/service.go | Service.ProcessLine — the call site for both paths, via forwardWebhookUrl and mCfg.LineWebhookURL |
internal/line/service.go | The httpDoer interface, already satisfied by *http.Client, which makes the client mockable in tests |
The HTTP client is a bare &http.Client{} constructed in line.Register. It deliberately does
not use the template's internal/httpclient, which provides retry and backoff, so timing is
controlled purely by the context timeout.
Connections to Other Services
- Redis —
forwardWebhookUrlandmboxLineWebhookUrlboth come from thewebhook_confighash (see Redis Cache for Webhook Config). - cms-api — the LINE OA management screen in the
line-oa-managementmodule is where this URL is configured; it maps to theline_oa.forward_webhook_urldatabase column. - The
line_forward_webhookqueue — declared in the topology viaRABBITMQ_QUEUE_LINE_FORWARD_WEBHOOK, but this service never publishes to it. Forwarding goes over plain HTTP without touching the queue, so it is likely a legacy queue or one used by worker-go. - No PostgreSQL dependency.