{"record":{"id":"03d0bc449501b78e","repo":"spree/spree","slug":"gateway-error-03d0bc","errorCode":"gateway_error","errorMessage":"Unable to connect to gateway.","messagePattern":"Unable to connect to gateway\\.","errorType":"exception","errorClass":"Spree::Core::GatewayError","httpStatus":422,"severity":"error","filePath":"spree/core/app/models/spree/refund.rb","lineNumber":154,"sourceCode":"        if payment.payment_method.payment_profiles_supported?\n          payment.payment_method.credit(refund_total_in_cents, payment.source, payment.transaction_id, originator: self)\n        else\n          payment.payment_method.credit(refund_total_in_cents, payment.transaction_id, originator: self)\n        end\n      end\n\n      if response.success?\n        track_order_as_refunded(refund_total_in_cents)\n      else\n        Rails.logger.error(Spree.t(:gateway_error) + \"  #{response.to_yaml}\")\n        text = response.params['message'] || response.params['response_reason_text'] || response.message\n        raise Core::GatewayError, text\n      end\n\n      response\n    rescue Spree::PaymentConnectionError => e\n      Rails.logger.error(Spree.t(:gateway_error) + \"  #{e.inspect}\")\n      raise Core::GatewayError, Spree.t(:unable_to_connect_to_gateway)\n    end\n\n    def calculate_refund_amount(credit_cents)\n      # Overwrite this for more complex calculations\n      credit_cents\n    end\n\n    def track_order_as_refunded(credit_cents)\n      # You can track refunds here\n    end\n\n    def amount_is_less_than_or_equal_to_allowed_amount\n      if amount > payment.credit_allowed\n        errors.add(:amount, :greater_than_allowed)\n      end\n    end\n\n    # Re-sums the order this refund put right. Read through the refund's own","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/spree/spree/blob/06bf66a8684b9de03210bbb2ddc8c1f5ba522fa2/spree/core/app/models/spree/refund.rb#L136-L172","documentation":"Raised by Spree::Refund#process! when the refund request never reaches a gateway response: the underlying payment call raises Spree::PaymentConnectionError (timeout, DNS failure, refused connection) and it is rescued and re-raised as Spree::Core::GatewayError with the translated 'unable_to_connect_to_gateway' text. It is distinct from a declined refund, which surfaces the gateway's own decline message instead. It means the state of the refund at the provider is unknown, not that the refund failed.","triggerScenarios":"Calling refund.process! / creating a refund through Payments::Refund while the app host cannot open a connection to the payment provider's API: network outage, egress firewall blocking the provider hostname, wrong API endpoint configured on the payment method, or the provider itself being down.","commonSituations":"Local development without internet access, staging servers behind restrictive proxies, a typo in the gateway URL preference, provider-side incidents, VPN dropping mid-request, or test suites accidentally hitting live gateway endpoints.","solutions":["Verify network egress from the app host to the payment provider API (curl the provider's API hostname) and fix firewall/proxy/VPN issues.","Check the payment method's configuration (mode, API endpoint, credentials) in the Admin API and correct any wrong values.","Before retrying, check the provider's dashboard (or API) for whether the refund actually landed despite the connection error — a timed-out request may still have been processed.","Retry the refund once connectivity is confirmed; if the provider is unreachable for an extended period, refund in the provider console and record it, or queue the refund for a background retry job."],"exampleFix":"# before\nrefund.process!(amount_in_cents)\n\n# after — handle connection loss distinctly from a decline\nbegin\n  refund.process!(amount_in_cents)\nrescue Spree::Core::GatewayError => e\n  if e.message == Spree.t(:unable_to_connect_to_gateway)\n    Rails.logger.warn(\"Refund #{refund.number} not confirmed at gateway: #{e.message}\")\n    RefundRetryJob.perform_later(refund.id, amount_in_cents) # verifies state at gateway before retrying\n  else\n    raise # real decline: surface it\n  end\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  refund.process!(amount_in_cents)\nrescue Spree::Core::GatewayError => e\n  if e.message == Spree.t(:unable_to_connect_to_gateway)\n    # outcome unknown: check the refund at the provider BEFORE retrying,\n    # then retry once from a background job — never blind-loop\n    RefundVerificationJob.perform_later(refund.id)\n  else\n    raise # decline or other gateway error: surface, do not retry\n  end\nend","preventionTips":["Monitor egress from app hosts to payment provider API domains and alert on connection-level failures.","Wrap refund execution in jobs with bounded retry so transient network blips self-heal without manual work.","Log the refunded payment's transaction id before attempting the gateway call so an unknown-outcome refund can be reconciled later.","Never auto-retry on an interval without first checking whether the refund landed at the provider — duplicate refunds are the real risk here."],"tags":["payments","refunds","network","gateway","spree-core"],"backgroundTag":"payment-gateway-unreachable","analyzedSha":"06bf66a8684b9de03210bbb2ddc8c1f5ba522fa2","analyzedAt":"2026-08-21T14:59:48.125Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}