Reputation: 684
Description
I have a simple e-commerce website where the frontend is built using React with TypeScript and Vite, hosted on Vercel. The backend is developed in native PHP and hosted on 000webhost's free plan. I'm using GraphQL for communication between the frontend and backend.
Problem
When making a POST request to my backend's GraphQL endpoint https://xxx.000webhostapp.com/graphql
from the frontend hosted on Vercel, the request fails in the browser but works fine in Postman. The error shown in the browser console is:
POST https://xxx.000webhostapp.com/graphql net::ERR_HTTP2_PROTOCOL_ERROR
Details
Frontend: React + TypeScript + Vite, hosted on Vercel
Backend: Native PHP, hosted on 000webhost free plan
GraphQL Client: urql
Local Development: POST requests work fine both from the website and Postman
Production Issue: POST request fails in the browser with ERR_HTTP2_PROTOCOL_ERROR
, but works in Postman.
Steps Taken
Verified CORS headers are correctly set on the backend (Access-Control-Allow-Origin, Access-Control-Allow-Headers, etc.). Confirmed that other GraphQL queries work fine in both development and production environments.
Ensured that the GraphQL endpoint URL is correct and accessible.
Tried using ApolloClient in frontend to send the request and still the same issue.
Question
Why am I encountering ERR_HTTP2_PROTOCOL_ERROR
only in the browser when making a GraphQL POST request to my backend, while it works fine in Postman? How can I troubleshoot and resolve this issue?
index.php
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php';
// Allow from any origin
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
}
$dispatcher = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/', function() {
return 'Hello from API'; // Return 'Hello' when accessing root via browser
});
$r->post('/graphql', [App\Controller\GraphQL::class, 'handle']);
});
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
$routeInfo = $dispatcher->dispatch(
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI']
);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo '404 Not Found';
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
http_response_code(405);
echo '405 Method Not Allowed';
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
echo call_user_func($handler, $vars);
break;
}
Definition of urql client in main.tsx
import ReactDOM from 'react-dom/client';
import App from './App';
import CartProvider from './context/CartContext';
import { Provider, createClient, cacheExchange, fetchExchange } from 'urql';
const apiURL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
console.log("API URL:", apiURL);
const client = createClient({
url: `${apiURL}/graphql`,
exchanges: [
cacheExchange,
fetchExchange,
]
});
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Failed to find the root element');
const root = ReactDOM.createRoot(rootElement);
// Render your application with CartProvider wrapping it
root.render(
<Provider value={client}>
<CartProvider>
<App />
</CartProvider>
</Provider>
);
Sample of a request made in HomePage.tsx
import { useParams } from 'react-router-dom';
import './HomePage.css';
import { useEffect, useState } from 'react';
import { useQuery } from 'urql'; // Import useQuery from urql
import { PRODUCTS_BY_CATEGORY_QUERY } from '../../queries/Queries.ts';
import ProductCard from '../../components/ProductCard/ProductCard.tsx';
const HomePage = () => {
const { category } = useParams();
const [products, setProducts] = useState<any[]>([]);
const [result] = useQuery({
query: PRODUCTS_BY_CATEGORY_QUERY,
variables: { categoryName: category },
pause: !category, // Skip query if category is not defined yet
});
const { fetching, error, data } = result;
useEffect(() => {
console.log('Fetching data for category:', category);
}, [category]);
useEffect(() => {
if (!fetching && data) {
setProducts(data.productsByCategory);
}
}, [fetching, data]);
if (fetching) return <div className="loading-icon"></div>; // Display spinner
if (error) return <p>Error: {error.message}</p>;
return (
<main className="home-page-main">
<div className="home-page">
<p className='category-name'>{category?.toUpperCase()}</p>
<div className="product-list">
{products.map((product, index) => (
<ProductCard key={index} product={product} />
))}
</div>
</div>
</main>
);
};
export default HomePage;
The query itself in Queries.ts
export const PRODUCTS_BY_CATEGORY_QUERY = `
query ProductsByCategory($categoryName: String!) {
productsByCategory(categoryName: $categoryName) {
id
name
in_stock
description
brand
category {
id
name
}
images {
id
url
}
attributes {
id
name
value
displayValue
}
prices {
id
amount
currency
}
}
}
`;
Upvotes: 0
Views: 131