Skip to main content

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)

  1. ProcessLine reads webhook_config and finds a non-empty forwardWebhookUrl.
  2. It calls forwardWebhook with the URL, headers, body, and raw bytes, without waiting for a result.
  3. 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

  1. When the user is in agent mode and the message is not an exit keyword, the message is forwarded to mboxLineWebhookUrl instead, so the mbox system can display it on the agent's screen (see Handoff to Human Agents).

Request details

  1. The URL is parsed first. If parsing fails, the service logs the warning Forward webhook failed: invalid url and abandons the send.
  2. A goroutine issues a POST to the target URL using context.WithTimeout set to 5 seconds.
  3. The body sent is BodyRaw, the original bytes. Only when no raw body exists does it marshal from the map.
  4. The only headers sent are content-type: application/json and x-line-signature (when present on the inbound request). No other headers are forwarded — no user-agent, no x-forwarded-for.
  5. req.Host is set to the target URL's host.
  6. 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.
  7. 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

FileFunction
internal/line/service.goService.forwardWebhook(rawURL, headers, body, raw) — the core implementation
internal/line/service.goService.ProcessLine — the call site for both paths, via forwardWebhookUrl and mCfg.LineWebhookURL
internal/line/service.goThe 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

  • RedisforwardWebhookUrl and mboxLineWebhookUrl both come from the webhook_config hash (see Redis Cache for Webhook Config).
  • cms-api — the LINE OA management screen in the line-oa-management module is where this URL is configured; it maps to the line_oa.forward_webhook_url database column.
  • The line_forward_webhook queue — declared in the topology via RABBITMQ_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.