DEV Community

sunah kim
sunah kim

Posted on Originally published at zenn.dev

We Migrated the Server and Only Payments Died — Reverse DNS and Error 908 from a Payment Gateway

The setup

While migrating a production EC-CUBE (PHP e-commerce) site to a new cloud instance, I clicked "confirm payment" on the new server and got this from the payment gateway (GMO Epsilon, a common Japanese payment provider):

ERR_CODE 908
"The order's source host information does not match the source IP address"
Enter fullscreen mode Exit fullscreen mode

The gateway's admin console already had the site's domain name and the old IPs registered as allowed sources. The domain's A record was already pointing at the new IP. So why was it rejected? Digging in, I found that the assumption "we registered the domain, so we're safe when the IP changes" had never actually worked — not even once.

Three traps

1. What I saw wasn't a 908 — it was a 500

The first visible symptom wasn't even a payment error. It was EC-CUBE's generic "system error" page (HTTP 500). The logs told the real story:

gmo_epsilon.ERROR: ... ERR_CODE=908 ERR_DETAIL=...
PHP Fatal error:  Call to undefined method Symfony\Component\HttpFoundation\Response::create()
Enter fullscreen mode Exit fullscreen mode

When the payment plugin receives a 908, it tries to build an error page with Response::create() — a static method removed in Symfony 6. So a payment-gateway error mutated into a framework fatal, and the 908 was invisible unless you opened the application log. First lesson: when payments fail, check the app log for the gateway's error code before anything else.

2. Hostname registration is compared via reverse DNS, suffix-match

Now the actual 908. Reading the gateway's manual carefully, the source-host check works like this:

  • A registered IP address is compared directly against the request's source IP
  • A registered hostname is compared, suffix-match, against the reverse DNS of the source IP

So even if you register shop.example.com, what gets compared is the reverse lookup of your server's IP. For a cloud IP, that's something like:

$ dig +short -x 203.0.113.10
ec2-203-0-113-10.ap-northeast-2.compute.amazonaws.com.
Enter fullscreen mode Exit fullscreen mode

That will never match your site's domain. The domain entry was pure decoration; the only thing that had ever let payments through was the IP entry. "The IP can change because we registered the domain" was never true.

3. Production and test allowlists are separate

One more 908 after registering the new IP: the admin console keeps separate source lists for production and test (switched via a "to test environment" link — same menu name, easy to miss). The dev server calls the beta endpoint, so it's checked against the test list. I had only added the IP to the production list at first.

The fix — append the new IP to the comma-separated list

The fix itself takes a minute: append the new IP to the comma-separated source-IP list and hit "update" — once for production, once for test.

The settings page is 13 bare <input name=url> fields, painful to verify by eye, so I dumped the values before and after from the browser console:

[...document.querySelectorAll('input[name=url]')].map(i => i.value)
Enter fullscreen mode Exit fullscreen mode

Verification — push one real order through before cutover

Reordering succeeded; the payment-completion callback showed up in the logs and 908s dropped to zero.

Before the production cutover, I also registered the new production IP ahead of time, added one line to my local /etc/hosts pointing the domain at the new server, and pushed one real order (bank-transfer type) through the new server before touching DNS. A payment outage on cutover day is the worst failure mode, so this is the one path worth pre-validating.

Takeaways

  • When a server's IP changes, "register the new source IP with the payment gateway" is item #1 on the checklist
  • Hostname registration is matched against reverse DNS, so it effectively never works for cloud IPs. "We registered the domain, so we're safe" doesn't hold
  • Production and test allowlists are separate. Dev servers hit the test list
  • Plugins can disguise gateway errors as framework 500s — read the app log's ERR_CODE first

The scariest part wasn't the outage. It was realizing that the "safe" domain-based entry had never worked a single day — and nobody noticed, because the IP entry quietly did all the work.

Top comments (0)