Gin CORS Error Blocking React Native Requests

A CORS error between a React Native app and a Gin backend is technically a browser-enforced restriction, but it commonly shows up during development with tools like Expo’s web preview, React Native Web, or when testing endpoints from a browser-based debugger — and the fix on the Gin side is the same regardless of exactly which client triggered it.

The Problem

Requests from your React Native app (or its web-based debugging tools) to your Gin API fail, and the browser console shows something like:

Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:19006' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

The endpoint works fine when tested directly with curl or Postman, which is expected — CORS is enforced by the browser, not the server, so tools that bypass browser fetch behavior won’t ever show this error even when the backend genuinely isn’t configured correctly.

Why It Happens

Gin doesn’t handle CORS out of the box — by default, it simply doesn’t send any Access-Control-* headers at all, which browsers interpret as “this origin is not allowed.” Common causes once you’ve added CORS handling include:

  • No CORS middleware is registered at all, so no Access-Control-Allow-Origin header is ever sent
  • The allowed origin is configured too strictly (e.g. hardcoded to a specific port that doesn’t match your current dev server’s actual port)
  • The CORS middleware is registered after the routes it’s supposed to protect, so it never actually runs before the request is handled
  • The request includes credentials (cookies or Authorization headers) but the server’s CORS config doesn’t explicitly allow credentials, which browsers block even if the origin itself is otherwise allowed

The Fix

Use the official Gin CORS middleware package rather than hand-rolling header logic, which is easy to get subtly wrong:

go get github.com/gin-contrib/cors

Register it before your routes, with an explicit configuration rather than the fully permissive default, especially if you need credentials support:

import "github.com/gin-contrib/cors"

router := gin.Default()
router.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"http://localhost:19006", "http://localhost:8081"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
    AllowCredentials: true,
}))

The order matters — this middleware must be registered with router.Use() before any route definitions, so it applies to every request, including the automatic OPTIONS preflight request browsers send before the real one for anything beyond a simple GET.

If you’re still in early development and just need something working quickly (not recommended for production), you can allow all origins explicitly:

router.Use(cors.New(cors.Config{
    AllowAllOrigins: true,
    AllowMethods:    []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:    []string{"Origin", "Content-Type", "Authorization"},
}))

Note that AllowAllOrigins and AllowCredentials: true cannot be used together — browsers reject that combination for security reasons, so if your requests include cookies or auth headers, you must list explicit origins instead of using the wildcard.

Still Not Working?

If the CORS headers look correct in a direct curl -I check but the browser still blocks the request, check the browser’s network tab specifically for the OPTIONS preflight request’s response, not just the actual GET/POST request — if the preflight itself fails or is missing the right headers, the browser never even attempts the real request, which can look identical to a general CORS misconfiguration but points to a more specific issue with how preflight responses are being handled.

It’s also worth checking whether another middleware registered before the CORS middleware is intercepting OPTIONS requests and responding to them directly, before Gin’s CORS middleware gets a chance to add its headers. Custom authentication middleware in particular is a common culprit, since it may reject an unauthenticated OPTIONS preflight request with a 401 before the actual CORS logic ever runs — browsers don’t send credentials on preflight requests, so an auth check placed too early in the middleware chain can silently break CORS for authenticated routes specifically, while unauthenticated routes work fine.

If you’re testing from a physical device or emulator rather than a browser-based tool, remember that CORS is a browser-only restriction — a genuine React Native app running on a device or in a native emulator doesn’t enforce CORS at all, since it isn’t going through a browser’s fetch implementation. If you’re seeing what looks like a CORS-style failure on-device, the actual cause is almost certainly something else, like an unreachable IP address (especially localhost, which refers to the device itself, not your development machine) rather than a genuine CORS policy violation.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *