Commit 69d6c7b4 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into 422-errors

parents cf84a4b9 c0f406be
...@@ -44,6 +44,8 @@ NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__ ...@@ -44,6 +44,8 @@ NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__ NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__ NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__ NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
# external services config # external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__ NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
......
...@@ -27,6 +27,14 @@ const baseUrl = [ ...@@ -27,6 +27,14 @@ const baseUrl = [
].filter(Boolean).join(''); ].filter(Boolean).join('');
const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl; const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl;
const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST); const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST);
const apiSchema = getEnvValue(process.env.NEXT_PUBLIC_API_PROTOCOL) || 'https';
const apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT);
const apiEndpoint = apiHost ? [
apiSchema || 'https',
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('') : 'https://blockscout.com';
const logoutUrl = (() => { const logoutUrl = (() => {
try { try {
...@@ -91,7 +99,7 @@ const config = Object.freeze({ ...@@ -91,7 +99,7 @@ const config = Object.freeze({
}, },
api: { api: {
host: apiHost, host: apiHost,
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com', endpoint: apiEndpoint,
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com', socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''), basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
}, },
......
This diff is collapsed.
...@@ -2,10 +2,11 @@ import { compile } from 'path-to-regexp'; ...@@ -2,10 +2,11 @@ import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { ApiResource } from './resources'; import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources';
export default function buildUrl( export default function buildUrl(
resource: ApiResource, _resource: ApiResource | ResourceName,
pathParams?: Record<string, string | undefined>, pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>, queryParams?: Record<string, string | number | undefined>,
) { ) {
...@@ -22,6 +23,7 @@ export default function buildUrl( ...@@ -22,6 +23,7 @@ export default function buildUrl(
// will need to change the condition if there are more micro services that need authentication and DB state changes // will need to change the condition if there are more micro services that need authentication and DB state changes
const needProxy = appConfig.host !== appConfig.api.host; const needProxy = appConfig.host !== appConfig.api.host;
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint); const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath; const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path; const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
......
import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources';
export default function buildUrlNode(
_resource: ApiResource | ResourceName,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
const baseUrl = resource.endpoint || appConfig.api.endpoint;
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, String(value));
});
return url.toString();
}
import type { NextApiRequest } from 'next';
export default function getSearchParams(req: NextApiRequest) {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
return new URLSearchParams(searchParams).toString();
}
import type { NextApiRequest } from 'next';
import appConfig from 'configs/app/config';
export default function getUrlWithNetwork(_req: NextApiRequest, path: string) {
return [
appConfig.api.basePath,
path,
]
.filter((segment) => segment !== '' && segment !== '/')
.join('');
}
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>, apiEndpoint?: string) {
const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
httpLogger(_req, res);
if (!_req.method || !allowedMethods.includes(_req.method as Methods)) {
res.setHeader('Allow', allowedMethods);
res.status(405).end(`Method ${ _req.method } Not Allowed`);
return;
}
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = apiEndpoint ? `/api${ getUrl(_req) }` : getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const fetch = fetchFactory(_req, apiEndpoint);
const response = await fetch(url, {
method: _req.method,
body: isBodyDisallowed ? undefined : _req.body,
});
if (response.status === 200) {
const data = await response.json();
res.status(200).json(data);
return;
}
let responseError;
const defaultError = { statusText: response.statusText, status: response.status };
try {
const error = await response.json() as { errors: unknown };
responseError = error?.errors || defaultError;
} catch (error) {
responseError = defaultError;
}
httpLogger.logger.error({ err: responseError, url: _req.url });
res.status(500).json(responseError);
};
return handler;
}
...@@ -2,25 +2,20 @@ import type { NextApiRequest } from 'next'; ...@@ -2,25 +2,20 @@ import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch'; import nodeFetch from 'node-fetch';
import appConfig from 'configs/app/config';
import { httpLogger } from 'lib/api/logger'; import { httpLogger } from 'lib/api/logger';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
export default function fetchFactory( export default function fetchFactory(
_req: NextApiRequest, _req: NextApiRequest,
apiEndpoint: string = appConfig.api.endpoint,
) { ) {
return function fetch(path: string, init?: RequestInit): Promise<Response> { // first arg can be only a string
const csrfToken = _req.headers['x-csrf-token']?.toString(); // FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> {
const headers = { const headers = {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
...(csrfToken ? { 'x-csrf-token': csrfToken } : {}),
}; };
const url = new URL(path, apiEndpoint);
httpLogger.logger.info({ httpLogger.logger.info({
message: 'Trying to call API', message: 'Trying to call API',
...@@ -28,7 +23,7 @@ export default function fetchFactory( ...@@ -28,7 +23,7 @@ export default function fetchFactory(
req: _req, req: _req,
}); });
return nodeFetch(url.toString(), { return nodeFetch(url, {
headers, headers,
...init, ...init,
}); });
......
...@@ -35,6 +35,9 @@ export interface ApiResource { ...@@ -35,6 +35,9 @@ export interface ApiResource {
export const RESOURCES = { export const RESOURCES = {
// ACCOUNT // ACCOUNT
csrf: {
path: '/api/account/v1/get_csrf',
},
user_info: { user_info: {
path: '/api/account/v1/user/info', path: '/api/account/v1/user/info',
}, },
...@@ -198,7 +201,6 @@ export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] ...@@ -198,7 +201,6 @@ export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R]
export const resourceKey = (x: keyof typeof RESOURCES) => x; export const resourceKey = (x: keyof typeof RESOURCES) => x;
export interface ResourceError<T = unknown> { export interface ResourceError<T = unknown> {
error?: T;
payload?: T; payload?: T;
status: Response['status']; status: Response['status'];
statusText: Response['statusText']; statusText: Response['statusText'];
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { CsrfData } from 'types/client/account'; import type { CsrfData } from 'types/client/account';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import { getResourceKey } from 'lib/api/useApiQuery';
export interface Params { export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
...@@ -15,7 +16,7 @@ export interface Params { ...@@ -15,7 +16,7 @@ export interface Params {
export default function useFetch() { export default function useFetch() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const reqParams = { const reqParams = {
...@@ -42,8 +43,6 @@ export default function useFetch() { ...@@ -42,8 +43,6 @@ export default function useFetch() {
return response.json().then( return response.json().then(
(jsonError) => Promise.reject({ (jsonError) => Promise.reject({
// DEPRECATED
error: jsonError as Error,
payload: jsonError as Error, payload: jsonError as Error,
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev:local": "./node_modules/.bin/dotenv -e ./configs/envs/.env.localhost -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty", "dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:goerli": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty", "dev:goerli": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"build": "next build", "build": "next build",
......
...@@ -23,7 +23,7 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -23,7 +23,7 @@ function MyApp({ Component, pageProps }: AppProps) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, _error) => { retry: (failureCount, _error) => {
const error = _error as ResourceError<{ status: number }>; const error = _error as ResourceError<{ status: number }>;
const status = error?.status || error?.error?.status; const status = error?.status || error?.payload?.status;
if (status && status >= 400 && status < 500) { if (status && status >= 400 && status < 500) {
// don't do retry for client error responses // don't do retry for client error responses
return false; return false;
......
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch'; import buildUrlNode from 'lib/api/buildUrlNode';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger'; import { httpLogger } from 'lib/api/logger';
import fetchFactory from 'lib/api/nodeFetch';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) { export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
httpLogger(_req, res); httpLogger(_req, res);
const url = getUrlWithNetwork(_req, `/api/account/v1/get_csrf`); const url = buildUrlNode('csrf');
const fetch = fetchFactory(_req); const response = await fetchFactory(_req)(url);
const response = await fetch(url);
if (response.status === 200) { if (response.status === 200) {
const token = response.headers.get('x-bs-account-csrf'); const token = response.headers.get('x-bs-account-csrf');
......
...@@ -2,7 +2,8 @@ import _pick from 'lodash/pick'; ...@@ -2,7 +2,8 @@ import _pick from 'lodash/pick';
import _pickBy from 'lodash/pickBy'; import _pickBy from 'lodash/pickBy';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch'; import appConfig from 'configs/app/config';
import fetchFactory from 'lib/api/nodeFetch';
const handler = async(_req: NextApiRequest, res: NextApiResponse) => { const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
if (!_req.url) { if (!_req.url) {
...@@ -10,8 +11,12 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => { ...@@ -10,8 +11,12 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
return; return;
} }
const response = await fetchFactory(_req, _req.headers['x-endpoint']?.toString())( const url = new URL(
_req.url.replace(/^\/node-api\/proxy/, ''), _req.url.replace(/^\/node-api\/proxy/, ''),
_req.headers['x-endpoint']?.toString() || appConfig.api.endpoint,
);
const response = await fetchFactory(_req)(
url.toString(),
_pickBy(_pick(_req, [ 'body', 'method' ]), Boolean), _pickBy(_pick(_req, [ 'body', 'method' ]), Boolean),
); );
......
...@@ -57,11 +57,11 @@ const BlockDetails = () => { ...@@ -57,11 +57,11 @@ const BlockDetails = () => {
} }
if (isError) { if (isError) {
if (error?.error?.status === 404) { if (error?.payload?.status === 404) {
return <span>This block has not been processed yet.</span>; return <span>This block has not been processed yet.</span>;
} }
if (error?.error?.status === 422) { if (error?.payload?.status === 422) {
throw Error('Invalid block number', { cause: error as unknown as Error }); throw Error('Invalid block number', { cause: error as unknown as Error });
} }
......
...@@ -2,8 +2,11 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,8 +2,11 @@ import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config';
import buildUrl from 'lib/api/buildUrl';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
// import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash'; import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
...@@ -24,22 +27,25 @@ const Page = ({ ...@@ -24,22 +27,25 @@ const Page = ({
hideMobileHeaderOnScrollDown, hideMobileHeaderOnScrollDown,
isHomePage, isHomePage,
}: Props) => { }: Props) => {
// const customFetch = useFetch(); const nodeApiFetch = useFetch();
useQuery([ 'csrf' ], async() => { useQuery(getResourceKey('csrf'), async() => {
// const nodeApiResponse = await customFetch('/node-api/csrf'); if (appConfig.host === appConfig.api.host) {
const apiResponse = await fetch('https://blockscout-main.test.aws-k8s.blockscout.com/api/account/v1/get_csrf', { credentials: 'include' }); const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
// eslint-disable-next-line no-console
console.log('>>> RESPONSE HEADERS <<<');
// eslint-disable-next-line no-console
console.table([ {
'content-length': apiResponse.headers.get('content-length'),
'x-bs-account-csrf': csrfFromHeader,
} ]);
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf'); return csrfFromHeader ? { token: csrfFromHeader } : undefined;
// eslint-disable-next-line no-console }
console.log('>>> RESPONSE HEADERS <<<');
// eslint-disable-next-line no-console
console.table([ {
'content-length': apiResponse.headers.get('content-length'),
'x-bs-account-csrf': csrfFromHeader,
} ]);
return csrfFromHeader ? { token: csrfFromHeader } : undefined; return nodeApiFetch('/node-api/csrf');
}, { }, {
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)), enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment