Native fetch wrapper in React and JavaScript

In modern web development with React, interacting with backend APIs is a fundamental task. While the browser’s built-in fetch API provides the basic functionality, it can become repetitive and cumbersome to handle tasks like setting headers, managing authentication, and implementing consistent error handling across your application. This blog post will guide you through building a reusable custom fetch client in React.

fetch-wrapper-in-react.png

Why Build a Custom Fetch Client?

Native fetch is flexible but barebones — it doesn’t handle common needs like:

  • Adding auth tokens to every request
  • Consistent error handling for different HTTP status codes
  • Parsing JSON, text, or blobs automatically
  • Centralized retry or logging logic

This client solves that with:

  • Request interceptors (attach tokens, headers)
  • Response interceptors (transform data, handle errors)
  • Custom error formatting
  • Helper methods for GET, POST, PUT, DELETE, and PATCH

Core Features

At the core of our custom client lies the FetchInstance class. This class acts as a wrapper around the native fetch API, providing a structured way to make HTTP requests and handle responses.

class FetchInstance {
	private baseURL: string;
	private requestInterceptors: Array<(config: RequestConfig) => RequestConfig | Promise<RequestConfig>> = [];
	private responseInterceptors: Array<(response: FetchResponse) => FetchResponse | Promise<FetchResponse>> = [];
	private errorInterceptors: Array<(error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>> = [];

	constructor(baseURL: string = import.meta.env.VITE_REACT_APP_BASE_URL) {
		this.baseURL = baseURL;
	}

	/**
	 * Add request interceptor
	 */
	interceptors = {
		// See full version at bottom  
	};

	/**
	 * Execute request with interceptors
	 */
	private async executeRequest(config: RequestConfig): Promise<FetchResponse> {
     // See full version at bottom 
	}

	/**
	 * GET request
	 */
	async get<T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
		return this.executeRequest({ ...config, method: 'GET', url });
	}

	/**
	 * POST request
	 */
	async post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
		const body = data ? JSON.stringify(data) : undefined;
		return this.executeRequest({ ...config, method: 'POST', url, body });
	}

	/**
	 * PUT request
	 */
	async put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
		const body = data ? JSON.stringify(data) : undefined;
		return this.executeRequest({ ...config, method: 'PUT', url, body });
	}

	/**
	 * DELETE request
	 */
	async delete<T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
		return this.executeRequest({ ...config, method: 'DELETE', url });
	}

	/**
	 * PATCH request
	 */
	async patch<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
		const body = data ? JSON.stringify(data) : undefined;
		return this.executeRequest({ ...config, method: 'PATCH', url, body });
	}

	/**
	 * Generic request method
	 */
	async request<T = any>(config: RequestConfig): Promise<FetchResponse<T>> {
		return this.executeRequest(config);
	}
}

1. Request Configuration

The client defines a RequestConfig type that extends RequestInit (the native fetch configuration object) with helpful extras like baseURL and params.

This ensures every request can be made with minimal configuration — you just specify the endpoint and any optional parameters, while the client handles the rest. For example, you can set a global baseURL so you only pass the relative endpoint (/users) in your calls. And response, error types are also added.

By typing the config, you also get IDE autocompletion and type safety, reducing mistakes.

  • baseURL – default API base
  • url – endpoint path
  • params – query parameters
/**
 * Interface for request configuration
 */
interface RequestConfig extends RequestInit {
	baseURL?: string;
	url?: string;
	params?: Record<string, any>;
}

/**
 * Interface for error response structure
 */
interface ErrorResponse {
	message: string;
	errorCode: string | null;
	status: string;
	statusCode: number;
	timestamp: string;
	errors: any[] | null;
	path: string | null;
}

/**
 * Interface for response structure
 */
interface FetchResponse<T = any> {
	data: T;
	status: number;
	statusText: string;
	headers: Headers;
	config: RequestConfig;
}

/**
 * Interface for error structure
 */
interface FetchError extends Error {
	statusCode: number;
	originalError: Error;
	response?: Response;
	request?: Request;
}

2. Default Error Messaging

A dedicated utility maps HTTP status codes to friendly, human-readable error messages. This avoids leaking raw API errors directly to end users and keeps messages consistent across the app.

For example, a 401 automatically returns “Authentication required. Please log in again.” while a 404 becomes “The requested resource was not found.”. This not only improves UX but also ensures that even unexpected API responses get handled gracefully.

The error mapping is extensible — you can add custom messages for domain-specific errors as your API evolves.

const getDefaultErrorMessage = (statusCode: number): string => {
	switch (statusCode) {
		case HTTP_401:
			return 'Authentication required. Please log in again.';
		case HTTP_403:
			return 'Access denied. You do not have permission to access this resource.';
		case HTTP_404:
			return 'The requested resource was not found.';
		case HTTP_405:
			return 'Method not allowed. This operation is not supported.';
		case HTTP_409:
			return 'Conflict. The request conflicts with the current state of the resource.';
		case HTTP_500:
			return 'Internal server error. Please try again later.';
		case HTTP_501:
			return 'Not implemented. This feature is not available.';
		case HTTP_502:
			return 'Bad gateway. The server received an invalid response.';
		case HTTP_503:
			return 'Service unavailable. The server is temporarily unavailable.';
		case HTTP_504:
			return 'Gateway timeout. The request timed out.';
		case HTTP_505:
			return 'HTTP version not supported.';
		default:
			return 'An unexpected error occurred.';
	}
};

3. Interceptors

One of the most powerful features is the ability to register request and response interceptors. Request interceptors let you modify or inspect the request before it’s sent — for example, attaching JWT tokens or setting default headers.

Response interceptors allow you to transform or log responses, or trigger special handling for specific status codes. This means you can catch a 401 and redirect to the login page globally without touching individual API calls.

This pattern is similar to Axios but built entirely on native fetch, giving you fine-grained control without external dependencies.

export const FetchInterceptor = (props: { children: React.JSX.Element }) => {
	const { idToken } = useOidcIdToken();
	const { login } = useOidc();

	// Request interceptor for API calls
	FetchClient.interceptors.request.use((config) => {
		// Attach the bearer token to the request header if available
		if (idToken) {
			config.headers = {
				...config.headers,
				Authorization: `Bearer ${idToken}`,
			};
		}

		// Set default headers
		config.headers = {
			'Content-Type': 'application/json',
			...config.headers,
		};

		return config;
	});

	// Response interceptor for API calls
	FetchClient.interceptors.response.use(
		(response: FetchResponse) => response,
		(error: FetchError) => {
			const statusCode = error.statusCode;

			// Handle 401 Unauthorized - redirect to login
			if (statusCode === HTTP_401) {
				login().then((r) => r);
				return;
			}

			// Handle other errors asynchronously
			(async () => {
				// Handle 403 Forbidden
				if (statusCode === HTTP_403) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_403);
					const formattedError = createFormattedError(error, HTTP_403, message, error.response);

					console.error('Access Forbidden:', message);
					return Promise.reject(formattedError);
				}

				// Handle 404 Not Found
				if (statusCode === HTTP_404) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_404);
					const formattedError = createFormattedError(error, HTTP_404, message, error.response);

					console.error('Resource Not Found:', message);
					return Promise.reject(formattedError);
				}

				// Handle 405 Method Not Allowed
				if (statusCode === HTTP_405) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_405);
					const formattedError = createFormattedError(error, HTTP_405, message, error.response);

					return Promise.reject(formattedError);
				}

				// Handle 409 Conflict
				if (statusCode === HTTP_409) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_409);
					const formattedError = createFormattedError(error, HTTP_409, message, error.response);

					return Promise.reject(formattedError);
				}

				// Handle 5xx Server Errors
				if (statusCode && statusCode >= 500 && statusCode <= 599) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(statusCode);
					const formattedError = createFormattedError(error, statusCode, message, error.response);

					console.error(`Server Error (${statusCode}):`, message);
					return Promise.reject(formattedError);
				}

				// Handle network errors (no response)
				if (!error.response) {
					const networkError = createFormattedError(error, 0, 'Network error. Please check your connection and try again.');

					console.error('Network Error:', error.message);
					return Promise.reject(networkError);
				}

				// Handle any other errors
				const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
				const message = errorResponse?.message || error.message || getDefaultErrorMessage(statusCode || 0);
				const formattedError = createFormattedError(error, statusCode || 0, message, error.response);

				console.error(`HTTP Error (${statusCode}):`, message);
				return Promise.reject(formattedError);
			})();
		}
	);

	return props.children;
};

4. Building URLs with Params

The client includes a utility that takes a base URL, an endpoint, and optional query parameters to build the full request URL. This eliminates repetitive string concatenation and manual query string encoding.

It ensures all query params are properly URL-encoded, which is especially important when dealing with special characters or spaces in parameter values. The function also works seamlessly for both absolute and relative URLs.

By centralizing this logic, you also reduce the risk of inconsistent parameter handling across different parts of the app.

const buildURL = (baseURL: string, url: string, params?: Record<string, any>): string => {
	const fullURL = url.startsWith('http') ? url : `${baseURL}${url}`;

	if (!params) return fullURL;

	const urlObj = new URL(fullURL);
	Object.entries(params).forEach(([key, value]) => {
		if (value !== undefined && value !== null) {
			urlObj.searchParams.append(key, String(value));
		}
	});

	return urlObj.toString();
};

5. Unified Response Parsing

The client automatically determines how to parse the response based on its Content-Type. JSON, plain text, and binary data (blobs) are all supported out of the box.

This avoids having to write repetitive response.json() or response.text() calls in every API request. It also ensures that any new response types your API introduces can be handled by updating a single function.

With this approach, your component logic receives already-parsed data, making it cleaner and more maintainable.

const parseResponseData = async (response: Response): Promise<any> => {
const contentType = response.headers.get('content-type');

if (contentType?.includes('application/json')) {
return await response.json();
} else if (contentType?.includes('text/')) {
return await response.text();
} else {
return await response.blob();
}
};

6. Error Handling

Every error is wrapped into a FetchError object containing the status code, the original error, and optionally the response and request.

This makes error handling predictable — components can check error.statusCode rather than manually parsing error messages. The client also uses interceptors to globally log, transform, or redirect based on specific errors.

In case of network failures, a friendly “Network error” message is returned, preventing cryptic browser errors from reaching the user. We wrap errors in a consistent FetchError object with:

  • statusCode
  • originalError
  • response
  • request

Auth Integration Example

In this example we are using @axa-fr/react-oidc, we inject the ID token into every request and redirect on 401. But you can use any library you like

export const FetchInterceptor = ({ children }) => {
  const { idToken } = useOidcIdToken();
  const { login } = useOidc();

  FetchClient.interceptors.request.use((config) => {
    if (idToken) config.headers = { ...config.headers, Authorization: `Bearer ${idToken}` };
    return config;
  });

  FetchClient.interceptors.response.use(null, (error) => {
    if (error.statusCode === 401) login();
  });

  return children;
};

Usage

Using the client is straightforward — you just call its helper methods for each HTTP verb. The client handles URL building, parsing, and error handling automatically.

The result object always contains data, status, statusText, and headers, making it easy to work with in your UI. This approach greatly reduces repetitive fetch boilerplate in your components.

Here’s a quick example:

// Simple GET
const res = await FetchClient.get('/users');
console.log(res.data);

// POST with body
await FetchClient.post('/users', { name: 'John' });

Benefits

Centralized API logic – Changes happen in one place.
Consistent error handling – No more scattered try/catch logic.
Seamless authentication integration – Tokens auto-attach, login auto-triggers.
No repeated boilerplate – Cleaner, smaller components.

With this reusable fetch client, your app gains a robust, maintainable HTTP layer without the weight of external dependencies.

Complete Code

import { useOidc, useOidcIdToken } from '@axa-fr/react-oidc';
import React from 'react';
import { HTTP_401, HTTP_403, HTTP_404, HTTP_405, HTTP_409, HTTP_500, HTTP_501, HTTP_502, HTTP_503, HTTP_504, HTTP_505 } from '../constants/HttpConstants';

/**
 * Interface for error response structure
 */
interface ErrorResponse {
	message: string;
	errorCode: string | null;
	status: string;
	statusCode: number;
	timestamp: string;
	errors: any[] | null;
	path: string | null;
}

/**
 * Interface for request configuration
 */
interface RequestConfig extends RequestInit {
	baseURL?: string;
	url?: string;
	params?: Record<string, any>;
}

/**
 * Interface for response structure
 */
interface FetchResponse<T = any> {
	data: T;
	status: number;
	statusText: string;
	headers: Headers;
	config: RequestConfig;
}

/**
 * Interface for error structure
 */
interface FetchError extends Error {
	statusCode: number;
	originalError: Error;
	response?: Response;
	request?: Request;
}

/**
 * Get default error message based on HTTP status code
 * @param statusCode HTTP status code
 * @returns Default error message
 */
const getDefaultErrorMessage = (statusCode: number): string => {
	switch (statusCode) {
		case HTTP_401:
			return 'Authentication required. Please log in again.';
		case HTTP_403:
			return 'Access denied. You do not have permission to access this resource.';
		case HTTP_404:
			return 'The requested resource was not found.';
		case HTTP_405:
			return 'Method not allowed. This operation is not supported.';
		case HTTP_409:
			return 'Conflict. The request conflicts with the current state of the resource.';
		case HTTP_500:
			return 'Internal server error. Please try again later.';
		case HTTP_501:
			return 'Not implemented. This feature is not available.';
		case HTTP_502:
			return 'Bad gateway. The server received an invalid response.';
		case HTTP_503:
			return 'Service unavailable. The server is temporarily unavailable.';
		case HTTP_504:
			return 'Gateway timeout. The request timed out.';
		case HTTP_505:
			return 'HTTP version not supported.';
		default:
			return 'An unexpected error occurred.';
	}
};

/**
 * Creates a formatted error object with additional context
 * @param error The original error
 * @param statusCode HTTP status code
 * @param message Error message
 * @param response Response object
 * @param request Request object
 * @returns Formatted error object
 */
const createFormattedError = (error: Error, statusCode: number, message: string, response?: Response, request?: Request): FetchError => {
	const formattedError = new Error(message) as FetchError;
	formattedError.statusCode = statusCode;
	formattedError.originalError = error;
	formattedError.response = response;
	formattedError.request = request;
	return formattedError;
};

/**
 * Build URL with query parameters
 * @param baseURL Base URL
 * @param url Endpoint URL
 * @param params Query parameters
 * @returns Complete URL with query parameters
 */
const buildURL = (baseURL: string, url: string, params?: Record<string, any>): string => {
	const fullURL = url.startsWith('http') ? url : `${baseURL}${url}`;

	if (!params) return fullURL;

	const urlObj = new URL(fullURL);
	Object.entries(params).forEach(([key, value]) => {
		if (value !== undefined && value !== null) {
			urlObj.searchParams.append(key, String(value));
		}
	});

	return urlObj.toString();
};

/**
 * Parse response data based on content type
 * @param response Fetch response
 * @returns Parsed data
 */
const parseResponseData = async (response: Response): Promise<any> => {
	const contentType = response.headers.get('content-type');

	if (contentType?.includes('application/json')) {
		return await response.json();
	} else if (contentType?.includes('text/')) {
		return await response.text();
	} else {
		return await response.blob();
	}
};

/**
 * Reusable fetch client class
 *
 * @author Pavan Kumar Jadda
 * @since 0.2.19
 */
class FetchInstance {
	private baseURL: string;
	private requestInterceptors: Array<(config: RequestConfig) => RequestConfig | Promise<RequestConfig>> = [];
	private responseInterceptors: Array<(response: FetchResponse) => FetchResponse | Promise<FetchResponse>> = [];
	private errorInterceptors: Array<(error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>> = [];

	constructor(baseURL: string = import.meta.env.VITE_REACT_APP_BASE_URL) {
		this.baseURL = baseURL;
	}

	/**
	 * Add request interceptor
	 */
	interceptors = {
		request: {
			use: (interceptor: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>) => {
				this.requestInterceptors.push(interceptor);
			},
		},
		response: {
			use: (
				successInterceptor?: (response: FetchResponse) => FetchResponse | Promise<FetchResponse>,
				errorInterceptor?: (error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>
			) => {
				if (successInterceptor) {
					this.responseInterceptors.push(successInterceptor);
				}
				if (errorInterceptor) {
					this.errorInterceptors.push(errorInterceptor);
				}
			},
		},
	};

	/**
	 * Execute request with interceptors
	 */
	private async executeRequest(config: RequestConfig): Promise<FetchResponse> {
		// Apply request interceptors
		let processedConfig = config;
		for (const interceptor of this.requestInterceptors) {
			processedConfig = await interceptor(processedConfig);
		}

		const { url, params, ...fetchConfig } = processedConfig;
		const fullURL = buildURL(this.baseURL, url || '', params);

		try {
			const response = await fetch(fullURL, fetchConfig);
			const data = await parseResponseData(response);

			const fetchResponse: FetchResponse = {
				data,
				status: response.status,
				statusText: response.statusText,
				headers: response.headers,
				config: processedConfig,
			};

			// Apply response interceptors
			let processedResponse = fetchResponse;
			for (const interceptor of this.responseInterceptors) {
				processedResponse = await interceptor(processedResponse);
			}

			return processedResponse;
		} catch (error) {
			const fetchError = createFormattedError(error as Error, 0, 'Network error. Please check your connection and try again.');

			// Apply error interceptors
			let processedError = fetchError;
			for (const interceptor of this.errorInterceptors) {
				const result = await interceptor(processedError);
				if (result !== undefined) {
					processedError = result;
				}
			}

			throw processedError;
		}
	}

	/**
	 * GET request
	 */
	async get<T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
		return this.executeRequest({ ...config, method: 'GET', url });
	}

	/**
	 * POST request
	 */
	async post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
		const body = data ? JSON.stringify(data) : undefined;
		return this.executeRequest({ ...config, method: 'POST', url, body });
	}

	/**
	 * PUT request
	 */
	async put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
		const body = data ? JSON.stringify(data) : undefined;
		return this.executeRequest({ ...config, method: 'PUT', url, body });
	}

	/**
	 * DELETE request
	 */
	async delete<T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
		return this.executeRequest({ ...config, method: 'DELETE', url });
	}

	/**
	 * PATCH request
	 */
	async patch<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
		const body = data ? JSON.stringify(data) : undefined;
		return this.executeRequest({ ...config, method: 'PATCH', url, body });
	}

	/**
	 * Generic request method
	 */
	async request<T = any>(config: RequestConfig): Promise<FetchResponse<T>> {
		return this.executeRequest(config);
	}
}

/**
 * Adds error handling interceptor
 *
 * @author Pavan Kumar Jadda
 * @since 1.1.1
 */
export const FetchInterceptor = (props: { children: React.JSX.Element }) => {
	const { idToken } = useOidcIdToken();
	const { login } = useOidc();

	// Request interceptor for API calls
	FetchClient.interceptors.request.use((config) => {
		// Attach the bearer token to the request header if available
		if (idToken) {
			config.headers = {
				...config.headers,
				Authorization: `Bearer ${idToken}`,
			};
		}

		// Set default headers
		config.headers = {
			'Content-Type': 'application/json',
			...config.headers,
		};

		return config;
	});

	// Response interceptor for API calls
	FetchClient.interceptors.response.use(
		(response: FetchResponse) => response,
		(error: FetchError) => {
			const statusCode = error.statusCode;

			// Handle 401 Unauthorized - redirect to login
			if (statusCode === HTTP_401) {
				login().then((r) => r);
				return;
			}

			// Handle other errors asynchronously
			(async () => {
				// Handle 403 Forbidden
				if (statusCode === HTTP_403) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_403);
					const formattedError = createFormattedError(error, HTTP_403, message, error.response);

					console.error('Access Forbidden:', message);
					return Promise.reject(formattedError);
				}

				// Handle 404 Not Found
				if (statusCode === HTTP_404) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_404);
					const formattedError = createFormattedError(error, HTTP_404, message, error.response);

					console.error('Resource Not Found:', message);
					return Promise.reject(formattedError);
				}

				// Handle 405 Method Not Allowed
				if (statusCode === HTTP_405) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_405);
					const formattedError = createFormattedError(error, HTTP_405, message, error.response);

					return Promise.reject(formattedError);
				}

				// Handle 409 Conflict
				if (statusCode === HTTP_409) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(HTTP_409);
					const formattedError = createFormattedError(error, HTTP_409, message, error.response);

					return Promise.reject(formattedError);
				}

				// Handle 5xx Server Errors
				if (statusCode && statusCode >= 500 && statusCode <= 599) {
					const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
					const message = errorResponse?.message || getDefaultErrorMessage(statusCode);
					const formattedError = createFormattedError(error, statusCode, message, error.response);

					console.error(`Server Error (${statusCode}):`, message);
					return Promise.reject(formattedError);
				}

				// Handle network errors (no response)
				if (!error.response) {
					const networkError = createFormattedError(error, 0, 'Network error. Please check your connection and try again.');

					console.error('Network Error:', error.message);
					return Promise.reject(networkError);
				}

				// Handle any other errors
				const errorResponse = error.response ? ((await parseResponseData(error.response)) as ErrorResponse) : null;
				const message = errorResponse?.message || error.message || getDefaultErrorMessage(statusCode || 0);
				const formattedError = createFormattedError(error, statusCode || 0, message, error.response);

				console.error(`HTTP Error (${statusCode}):`, message);
				return Promise.reject(formattedError);
			})();
		}
	);

	return props.children;
};

// Remove the callable FetchClient export and only export the instance
const FetchClient = new FetchInstance();

// Export the client instance for direct use
export default FetchClient;

Pavan Jadda
Pavan Jadda
Articles: 3

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.