最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

reactjs - Server side routing fails within Docker - Stack Overflow

programmeradmin1浏览0评论

Context

  • I have a Vike/React application that sends request to a backend API via Tanstack React-Query/Axios.
  • I recently implemented SSR functionality via a migration from Vite to Vike.

The Problem

When running my application in a local terminal outside Docker, I'm having no problem. However, after creating a Docker implementation, it appears to be failing with SSR. I have a page at endpoint /blog that makes a call to the backend API and renders the content server side before returning a response.

When I use curl on the endpoint...

  1. Without Docker, the content fetched server-side from the backend API appears properly in the HTML.
  2. With Docker, the content does not appear in the HTML and I get the 500 Internal Server Error fallback.

If I navigate to /blog via the client side, the content is properly rendered on the page. But, if I reload the page, it immediately fails and I get the fallback page.

What I've Tried

  • Using 127.0.0.1 and the name of the container instead of localhost when calling my backend APIs.
  • Exposing the 24678 port for the websocket connection.
  • Reinstalling node-modules.
  • Logging the PageContext and checking for errors via Axios.
  • Verified that my backend API does not receive any call when I navigate to the /blog endpoint directly in my browser.

Code

Here is some of the relevant code.

index.ts (Express server)

import { createDevMiddleware, renderPage } from 'vike/server';

import compression from 'compression';
import express from 'express';

import { root } from './root.ts';

const isProduction = process.env.NODE_ENV === 'production';

startServer();

export async function startServer() {
    const app = express();

    app.use(compression())

    // Vite integration
    if (isProduction) {
        // In production, we need to serve our static assets ourselves.
        // (In dev, Vite's middleware serves our static assets.)
        const sirv = (await import('sirv')).default;
        app.use(sirv(`${root}/public`))
    } else {
        const { devMiddleware } = await createDevMiddleware({ root });
        app.use(devMiddleware);
    }

    // Vike middleware. It should always be our last middleware (because it's a
    // catch-all middleware superseding any middleware placed after it).
    app.get('*', async (req, res) => {
        const pageContextInit = {
            urlOriginal: `${req.protocol}://${req.get('host')}` + req.originalUrl, // This will provide the full page URL to Vike's PageContext
            headersOriginal: req.headers
        }
        const pageContext = await renderPage(pageContextInit);
        if (pageContext.httpResponse.pipe) {
            pageContext.httpResponse.pipe(res)
        } else {
            res.status(pageContext.httpResponse.statusCode).send(pageContext.httpResponse.body)
        }
    })
    const port = process.env.PORT || 5174
    app.listen(port)
    console.log(`Server running at http://localhost:${port}`)
}

package.json "scripts"

"scripts": {
    "dev": "npm run server:dev",
    "prod": "npm run build && npm run server:prod",
    "build": "vite build && tsc",
    "server": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' ./server/index.ts",
    "server:dev": "npm run server",
    "server:prod": "cross-env NODE_ENV=production npm run server",
    "lint": "eslint . --max-warnings 0"
  }

Blog Page component

import { useConfig } from "vike-react/useConfig";
import { usePageContext } from "vike-react/usePageContext";

import { useSuspenseQuery } from "@tanstack/react-query";

import { withFallback } from "vike-react-query";

import ms from "ms";

import { Box, Divider, Flex, HStack, Tag, Text } from "@chakra-ui/react";

import ChakraMarkdown from "../../../../../../components/ChakraMarkdown";
import CustomMetaTags from "../../../../../../components/CustomMetaTags";
import GenericPage from "../../../../../../components/GenericPage";
import Title from "../../../../../../components/Title";
import BlogPost from "../../../../../../entities/BlogPost";
import APIClient, {
  blogPostApiV1AxiosInstance,
} from "../../../../../../services/apiClient";
import { getMonthName } from "../../../../../../utils/utilities";

const blogPostApiClient = new APIClient<BlogPost>(blogPostApiV1AxiosInstance);
const cacheTtl = import.meta.env.VIKE_CACHE_TTL || "10s"; // 10s default cache time

// Blog Post Page

const Page = withFallback(
  // Component if query is successful
  () => {
    const pageContext = usePageContext();
    const { year, month, day, slug } = pageContext.routeParams;

    // Query data
    const query = useSuspenseQuery({
      queryKey: ["blogPost", slug],
      queryFn: () =>
        blogPostApiClient.get(`/blogpost/${year}/${month}/${day}/${slug}`),
      staleTime: ms(cacheTtl as string),
    });
    const blogPost = query.data?.content;

    // Set meta tags
    const config = useConfig();
    config({
      title: `${blogPost?.title} | Fernando Sesma`,
      description: blogPost?.subtitle,
      image: blogPost?.thumbnailUrl,
      Head: <CustomMetaTags pageContext={pageContext} isArticle={true} />,
    });

    return (
      <>
        <GenericPage
          isReturnable={true}
          isReturnableLink="/blog"
          pageWidth="700px"
        >
          <Box>
            <Title titleText={blogPost?.title || ""} />
          </Box>
          <Box>
            <Text fontSize="lg">{blogPost?.subtitle || ""}</Text>
          </Box>
          <Flex justifyContent="space-between" alignItems="center" pt={2}>
            <HStack justify="flex-end" flexWrap="wrap">
              {blogPost?.tags.length !== 0 &&
                blogPost?.tags.map((tag) => <Tag key={tag}>{tag}</Tag>)}
            </HStack>
            <Text>{getMonthName(Number(month)) + ` ${day}, ${year}`}</Text>
          </Flex>
          <Box pt={5} pb={5}>
            <Divider />
          </Box>
          <ChakraMarkdown content={blogPost?.postContent} />
        </GenericPage>
      </>
    );
  },
  // Loading
  // TODO: Make a loading component for the blog post page
  undefined,
  ({ error }) => (
    <GenericPage isReturnable={true} isReturnableLink="/blog" pageWidth="700px">
      <Text fontSize="lg">{`An error occurred. Message: ${error.message}.`}</Text>
    </GenericPage>
  )
);

export default Page;

Dockerfile

FROM node:alpine
LABEL authors="fernandosesma"

WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH

COPY package.json ./

RUN npm install --silent

docker-compose.yaml

services:
  ferns-web:
    image: ferns-web:latest
    build: .
    command: npm run dev # 'dev' can be overriden with another profile to use
    container_name: ferns-web
    labels:
      service: ferns-web # Labels required for deployment scripts
    ports:
      - "5174:5173"
      - "24678:24678" # Expose these ports for WebSocket
    volumes:
      - .:/app
      # 
      # For now we need to mount node_modules to the container from the project root
      - /app/node_modules
    environment:
      - PUBLIC_ENV__VIKE_BLOG_POST_API_URL=http://localhost:8081
      - PUBLIC_ENV__VIKE_MEDIA_API_URL=http://localhost:8082
      - PORT=5173

volumes:
  node_modules: {}

Axios error in console for React app (this is what appears when I navigate directly to /blog

2025-02-10 11:09:29 5:09:29 PM [vike][request(2)] Following error was thrown by the onRenderHtml() hook defined at vike-react/__internal/integration/onRenderHtml
2025-02-10 11:09:29 AxiosError [AggregateError]: 
2025-02-10 11:09:29     at Function.AxiosError.from (file:///app/node_modules/axios/lib/core/AxiosError.js:92:14)
2025-02-10 11:09:29     at RedirectableRequest.handleRequestError (file:///app/node_modules/axios/lib/adapters/http.js:620:25)
2025-02-10 11:09:29     at RedirectableRequest.emit (node:events:507:28)
2025-02-10 11:09:29     at ClientRequest.eventHandlers.<computed> (/app/node_modules/follow-redirects/index.js:49:24)
2025-02-10 11:09:29     at ClientRequest.emit (node:events:507:28)
2025-02-10 11:09:29     at emitErrorEvent (node:_http_client:104:11)
2025-02-10 11:09:29     at Socket.socketErrorListener (node:_http_client:518:5)
2025-02-10 11:09:29     at Socket.emit (node:events:507:28)
2025-02-10 11:09:29     at emitErrorNT (node:internal/streams/destroy:170:8)
2025-02-10 11:09:29     at emitErrorCloseNT (node:internal/streams/destroy:129:3)
2025-02-10 11:09:29     at Axios.request (file:///app/node_modules/axios/lib/core/Axios.js:45:41)
2025-02-10 11:09:29     at processTicksAndRejections (node:internal/process/task_queues:105:5)
2025-02-10 11:09:29     at APIClient.getAll (/app/src/services/apiClient.ts:36:21) {
2025-02-10 11:09:29   code: 'ECONNREFUSED',
2025-02-10 11:09:29   errors: [
2025-02-10 11:09:29     Error: connect ECONNREFUSED ::1:8081
2025-02-10 11:09:29         at createConnectionError (node:net:1675:14)
2025-02-10 11:09:29         at afterConnectMultiple (node:net:1705:16)
2025-02-10 11:09:29         at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
2025-02-10 11:09:29       errno: -111,
2025-02-10 11:09:29       code: 'ECONNREFUSED',
2025-02-10 11:09:29       syscall: 'connect',
2025-02-10 11:09:29       address: '::1',
2025-02-10 11:09:29       port: 8081
2025-02-10 11:09:29     },
2025-02-10 11:09:29     Error: connect ECONNREFUSED 127.0.0.1:8081
2025-02-10 11:09:29         at createConnectionError (node:net:1675:14)
2025-02-10 11:09:29         at afterConnectMultiple (node:net:1705:16)
2025-02-10 11:09:29         at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
2025-02-10 11:09:29       errno: -111,
2025-02-10 11:09:29       code: 'ECONNREFUSED',
2025-02-10 11:09:29       syscall: 'connect',
2025-02-10 11:09:29       address: '127.0.0.1',
2025-02-10 11:09:29       port: 8081
2025-02-10 11:09:29     }
2025-02-10 11:09:29   ],
2025-02-10 11:09:29   config: {
2025-02-10 11:09:29     transitional: {
2025-02-10 11:09:29       silentJSONParsing: true,
2025-02-10 11:09:29       forcedJSONParsing: true,
2025-02-10 11:09:29       clarifyTimeoutError: false
2025-02-10 11:09:29     },
2025-02-10 11:09:29     adapter: [ 'xhr', 'http', 'fetch' ],
2025-02-10 11:09:29     transformRequest: [ [Function: transformRequest] ],
2025-02-10 11:09:29     transformResponse: [ [Function: transformResponse] ],
2025-02-10 11:09:29     timeout: 0,
2025-02-10 11:09:29     xsrfCookieName: 'XSRF-TOKEN',
2025-02-10 11:09:29     xsrfHeaderName: 'X-XSRF-TOKEN',
2025-02-10 11:09:29     maxContentLength: -1,
2025-02-10 11:09:29     maxBodyLength: -1,
2025-02-10 11:09:29     env: { FormData: [Function], Blob: [class Blob] },
2025-02-10 11:09:29     validateStatus: [Function: validateStatus],
2025-02-10 11:09:29     headers: Object [AxiosHeaders] {
2025-02-10 11:09:29       Accept: 'application/json, text/plain, */*',
2025-02-10 11:09:29       'Content-Type': undefined,
2025-02-10 11:09:29       'User-Agent': 'axios/1.7.9',
2025-02-10 11:09:29       'Accept-Encoding': 'gzip, compress, deflate, br'
2025-02-10 11:09:29     },
2025-02-10 11:09:29     baseURL: 'http://localhost:8081/api/v1',
2025-02-10 11:09:29     params: { page: 0, size: 6, sort: 'desc' },
2025-02-10 11:09:29     method: 'get',
2025-02-10 11:09:29     url: '/blogpost/getall',
2025-02-10 11:09:29     data: undefined
2025-02-10 11:09:29   },
2025-02-10 11:09:29   request: <ref *1> Writable {
2025-02-10 11:09:29     _events: {
2025-02-10 11:09:29       close: undefined,
2025-02-10 11:08:43 page context: [object Object]
2025-02-10 11:09:28 5:09:28 PM [vike][request(2)] HTTP request: /blog
2025-02-10 11:09:29 page context: [object Object]
2025-02-10 11:09:29       error: [Function: handleRequestError],
2025-02-10 11:09:29       prefinish: undefined,
2025-02-10 11:09:29       finish: undefined,
2025-02-10 11:09:29       drain: undefined,
2025-02-10 11:09:29       response: [Function: handleResponse],
2025-02-10 11:09:29       socket: [Function: handleRequestSocket]
2025-02-10 11:09:29     },
2025-02-10 11:09:29     _writableState: WritableState {
2025-02-10 11:09:29       highWaterMark: 65536,
2025-02-10 11:09:29       length: 0,
2025-02-10 11:09:29       corked: 0,
2025-02-10 11:09:29       onwrite: [Function: bound onwrite],
2025-02-10 11:09:29       writelen: 0,
2025-02-10 11:09:29       bufferedIndex: 0,
2025-02-10 11:09:29       pendingcb: 0,
2025-02-10 11:09:29       [Symbol(kState)]: 17580812,
2025-02-10 11:09:29       [Symbol(kBufferedValue)]: null
2025-02-10 11:09:29     },
2025-02-10 11:09:29     _maxListeners: undefined,
2025-02-10 11:09:29     _options: {
2025-02-10 11:09:29       maxRedirects: 21,
2025-02-10 11:09:29       maxBodyLength: Infinity,
2025-02-10 11:09:29       protocol: 'http:',
2025-02-10 11:09:29       path: '/api/v1/blogpost/getall?page=0&size=6&sort=desc',
2025-02-10 11:09:29       method: 'GET',
2025-02-10 11:09:29       headers: [Object: null prototype],
2025-02-10 11:09:29       agents: [Object],
2025-02-10 11:09:29       auth: undefined,
2025-02-10 11:09:29       family: undefined,
2025-02-10 11:09:29       beforeRedirect: [Function: dispatchBeforeRedirect],
2025-02-10 11:09:29       beforeRedirects: [Object],
2025-02-10 11:09:29       hostname: 'localhost',
2025-02-10 11:09:29       port: '8081',
2025-02-10 11:09:29       agent: undefined,
2025-02-10 11:09:29       nativeProtocols: [Object],
2025-02-10 11:09:29       pathname: '/api/v1/blogpost/getall',
2025-02-10 11:09:29       search: '?page=0&size=6&sort=desc'
2025-02-10 11:09:29     },
2025-02-10 11:09:29     _ended: true,
2025-02-10 11:09:29     _ending: true,
2025-02-10 11:09:29     _redirectCount: 0,
2025-02-10 11:09:29     _redirects: [],
2025-02-10 11:09:29     _requestBodyLength: 0,
2025-02-10 11:09:29     _requestBodyBuffers: [],
2025-02-10 11:09:29     _eventsCount: 3,
2025-02-10 11:09:29     _onNativeResponse: [Function (anonymous)],
2025-02-10 11:09:29     _currentRequest: ClientRequest {
2025-02-10 11:09:29       _events: [Object: null prototype],
2025-02-10 11:09:29       _eventsCount: 7,
2025-02-10 11:09:29       _maxListeners: undefined,
2025-02-10 11:09:29       outputData: [],
2025-02-10 11:09:29       outputSize: 0,
2025-02-10 11:09:29       writable: true,
2025-02-10 11:09:29       destroyed: true,
2025-02-10 11:09:29       _last: true,
2025-02-10 11:09:29       chunkedEncoding: false,
2025-02-10 11:09:29       shouldKeepAlive: true,
2025-02-10 11:09:29       maxRequestsOnConnectionReached: false,
2025-02-10 11:09:29       _defaultKeepAlive: true,
2025-02-10 11:09:29       useChunkedEncodingByDefault: false,
2025-02-10 11:09:29       sendDate: false,
2025-02-10 11:09:29       _removedConnection: false,
2025-02-10 11:09:29       _removedContLen: false,
2025-02-10 11:09:29       _removedTE: false,
2025-02-10 11:09:29       strictContentLength: false,
2025-02-10 11:09:29       _contentLength: 0,
2025-02-10 11:09:29       _hasBody: true,
2025-02-10 11:09:29       _trailer: '',
2025-02-10 11:09:29       finished: true,
2025-02-10 11:09:29       _headerSent: true,
2025-02-10 11:09:29       _closed: true,
2025-02-10 11:09:29       _header: 'GET /api/v1/blogpost/getall?page=0&size=6&sort=desc HTTP/1.1\r\n' +
2025-02-10 11:09:29         'Accept: application/json, text/plain, */*\r\n' +
2025-02-10 11:09:29         'User-Agent: axios/1.7.9\r\n' +
2025-02-10 11:09:29         'Accept-Encoding: gzip, compress, deflate, br\r\n' +
2025-02-10 11:09:29         'Host: localhost:8081\r\n' +
2025-02-10 11:09:29         'Connection: keep-alive\r\n' +
2025-02-10 11:09:29         '\r\n',
2025-02-10 11:09:29       _keepAliveTimeout: 0,
2025-02-10 11:09:29       _onPendingData: [Function: nop],
2025-02-10 11:09:29       agent: [Agent],
2025-02-10 11:09:29       socketPath: undefined,
2025-02-10 11:09:29       method: 'GET',
2025-02-10 11:09:29       maxHeaderSize: undefined,
2025-02-10 11:09:29       insecureHTTPParser: undefined,
2025-02-10 11:09:29       joinDuplicateHeaders: undefined,
2025-02-10 11:09:29       path: '/api/v1/blogpost/getall?page=0&size=6&sort=desc',
2025-02-10 11:09:29       _ended: false,
2025-02-10 11:09:29       res: null,
2025-02-10 11:09:29       aborted: false,
2025-02-10 11:09:29       timeoutCb: [Function: emitRequestTimeout],
2025-02-10 11:09:29       upgradeOrConnect: false,
2025-02-10 11:09:29       parser: null,
2025-02-10 11:09:29       maxHeadersCount: null,
2025-02-10 11:09:29       reusedSocket: false,
2025-02-10 11:09:29       host: 'localhost',
2025-02-10 11:09:29       protocol: 'http:',
2025-02-10 11:09:29       _redirectable: [Circular *1],
2025-02-10 11:09:29       [Symbol(shapeMode)]: false,
2025-02-10 11:09:29       [Symbol(kCapture)]: false,
2025-02-10 11:09:29       [Symbol(kBytesWritten)]: 0,
2025-02-10 11:09:29       [Symbol(kNeedDrain)]: false,
2025-02-10 11:09:29       [Symbol(corked)]: 0,
2025-02-10 11:09:29       [Symbol(kChunkedBuffer)]: [],
2025-02-10 11:09:29       [Symbol(kChunkedLength)]: 0,
2025-02-10 11:09:29       [Symbol(kSocket)]: [Socket],
2025-02-10 11:09:29       [Symbol(kOutHeaders)]: [Object: null prototype],
2025-02-10 11:09:29       [Symbol(errored)]: null,
2025-02-10 11:09:29       [Symbol(kHighWaterMark)]: 65536,
2025-02-10 11:09:29       [Symbol(kRejectNonStandardBodyWrites)]: false,
2025-02-10 11:09:29       [Symbol(kUniqueHeaders)]: null
2025-02-10 11:09:29     },
2025-02-10 11:09:29     _currentUrl: 'http://localhost:8081/api/v1/blogpost/getall?page=0&size=6&sort=desc',
2025-02-10 11:09:29     [Symbol(shapeMode)]: true,
2025-02-10 11:09:29     [Symbol(kCapture)]: false
2025-02-10 11:09:29   },
2025-02-10 11:09:29   cause: AggregateError: 
2025-02-10 11:09:29       at internalConnectMultiple (node:net:1139:18)
2025-02-10 11:09:29       at afterConnectMultiple (node:net:1712:7)
2025-02-10 11:09:29       at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
2025-02-10 11:09:29     code: 'ECONNREFUSED',
2025-02-10 11:09:29     [errors]: [ [Error], [Error] ]
2025-02-10 11:09:29   }
2025-02-10 11:09:29 }
2025-02-10 11:09:29 5:09:29 PM [vike][request(2)] HTTP response /blog 500

apiClient.ts

import axios, { AxiosInstance, AxiosRequestConfig } from "axios";

export interface FetchResponse<T> {
    content: T[];
}

export interface FetchResponseSingle<T> {
    content: T;
}

const blogPostApiUrl = import.meta.env.PUBLIC_ENV__VIKE_BLOG_POST_API_URL;
const mediaApiUrl = import.meta.env.PUBLIC_ENV__VIKE_MEDIA_API_URL;

// Currently using version 1 of all APIs for both blog post and media services
const blogPostApiV1AxiosInstance = axios.create({
    baseURL: blogPostApiUrl + '/api/v1'
});
const mediaApiV1AxiosInstance = axios.create({
    baseURL: mediaApiUrl + '/v1'
});

class APIClient<T> {

    axiosInstance: AxiosInstance;

    constructor(axiosInstance: AxiosInstance) {
        this.axiosInstance = axiosInstance;
    }

    get = async (endpoint: string) => {
        const res = await this.axiosInstance.get<FetchResponseSingle<T>>(`${endpoint}`);
        return res.data;
    }

    getAll = async (endpoint: string, config: AxiosRequestConfig) => {
        const res = await this.axiosInstance.get<FetchResponse<T>>(`${endpoint}`, config);
        return res.data;
    }

}

export { blogPostApiV1AxiosInstance, mediaApiV1AxiosInstance };

export default APIClient;

Any help is greatly appreciated, thanks!

发布评论

评论列表(0)

  1. 暂无评论
ok 不同模板 switch ($forum['model']) { /*case '0': include _include(APP_PATH . 'view/htm/read.htm'); break;*/ default: include _include(theme_load('read', $fid)); break; } } break; case '10': // 主题外链 / thread external link http_location(htmlspecialchars_decode(trim($thread['description']))); break; case '11': // 单页 / single page $attachlist = array(); $imagelist = array(); $thread['filelist'] = array(); $threadlist = NULL; $thread['files'] > 0 and list($attachlist, $imagelist, $thread['filelist']) = well_attach_find_by_tid($tid); $data = data_read_cache($tid); empty($data) and message(-1, lang('data_malformation')); $tidlist = $forum['threads'] ? page_find_by_fid($fid, $page, $pagesize) : NULL; if ($tidlist) { $tidarr = arrlist_values($tidlist, 'tid'); $threadlist = well_thread_find($tidarr, $pagesize); // 按之前tidlist排序 $threadlist = array2_sort_key($threadlist, $tidlist, 'tid'); } $allowpost = forum_access_user($fid, $gid, 'allowpost'); $allowupdate = forum_access_mod($fid, $gid, 'allowupdate'); $allowdelete = forum_access_mod($fid, $gid, 'allowdelete'); $access = array('allowpost' => $allowpost, 'allowupdate' => $allowupdate, 'allowdelete' => $allowdelete); $header['title'] = $thread['subject']; $header['mobile_link'] = $thread['url']; $header['keywords'] = $thread['keyword'] ? $thread['keyword'] : $thread['subject']; $header['description'] = $thread['description'] ? $thread['description'] : $thread['brief']; $_SESSION['fid'] = $fid; if ($ajax) { empty($conf['api_on']) and message(0, lang('closed')); $apilist['header'] = $header; $apilist['extra'] = $extra; $apilist['access'] = $access; $apilist['thread'] = well_thread_safe_info($thread); $apilist['thread_data'] = $data; $apilist['forum'] = $forum; $apilist['imagelist'] = $imagelist; $apilist['filelist'] = $thread['filelist']; $apilist['threadlist'] = $threadlist; message(0, $apilist); } else { include _include(theme_load('single_page', $fid)); } break; default: message(-1, lang('data_malformation')); break; } ?>