Mohamed Darwesh
Mohamed Darwesh

Reputation: 684

net::ERR_HTTP2_PROTOCOL_ERROR in React when sending GraphQL request to PHP backend hosted on 000webhost (Vercel frontend)

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


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

Answers (0)

Related Questions