entry.server.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { PassThrough } from "node:stream";
  2. import type { AppLoadContext, EntryContext } from "@remix-run/node";
  3. import { createReadableStreamFromReadable } from "@remix-run/node";
  4. import { RemixServer } from "@remix-run/react";
  5. import * as isbotModule from "isbot";
  6. import { renderToPipeableStream } from "react-dom/server";
  7. const ABORT_DELAY = 5_000;
  8. export default function handleRequest(
  9. request: Request,
  10. responseStatusCode: number,
  11. responseHeaders: Headers,
  12. remixContext: EntryContext,
  13. loadContext: AppLoadContext
  14. ) {
  15. return isBotRequest(request.headers.get("user-agent"))
  16. ? handleBotRequest(
  17. request,
  18. responseStatusCode,
  19. responseHeaders,
  20. remixContext
  21. )
  22. : handleBrowserRequest(
  23. request,
  24. responseStatusCode,
  25. responseHeaders,
  26. remixContext
  27. );
  28. }
  29. // We have some Remix apps in the wild already running with isbot@3 so we need
  30. // to maintain backwards compatibility even though we want new apps to use
  31. // isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev.
  32. function isBotRequest(userAgent: string | null) {
  33. if (!userAgent) {
  34. return false;
  35. }
  36. // isbot >= 3.8.0, >4
  37. if ("isbot" in isbotModule && typeof isbotModule.isbot === "function") {
  38. return isbotModule.isbot(userAgent);
  39. }
  40. // isbot < 3.8.0
  41. if ("default" in isbotModule && typeof isbotModule.default === "function") {
  42. return isbotModule.default(userAgent);
  43. }
  44. return false;
  45. }
  46. function handleBotRequest(
  47. request: Request,
  48. responseStatusCode: number,
  49. responseHeaders: Headers,
  50. remixContext: EntryContext
  51. ) {
  52. return new Promise((resolve, reject) => {
  53. let shellRendered = false;
  54. const { pipe, abort } = renderToPipeableStream(
  55. <RemixServer
  56. context={remixContext}
  57. url={request.url}
  58. abortDelay={ABORT_DELAY}
  59. />,
  60. {
  61. onAllReady() {
  62. shellRendered = true;
  63. const body = new PassThrough();
  64. const stream = createReadableStreamFromReadable(body);
  65. responseHeaders.set("Content-Type", "text/html");
  66. resolve(
  67. new Response(stream, {
  68. headers: responseHeaders,
  69. status: responseStatusCode,
  70. })
  71. );
  72. pipe(body);
  73. },
  74. onShellError(error: unknown) {
  75. reject(error);
  76. },
  77. onError(error: unknown) {
  78. responseStatusCode = 500;
  79. // Log streaming rendering errors from inside the shell. Don't log
  80. // errors encountered during initial shell rendering since they'll
  81. // reject and get logged in handleDocumentRequest.
  82. if (shellRendered) {
  83. console.error(error);
  84. }
  85. },
  86. }
  87. );
  88. setTimeout(abort, ABORT_DELAY);
  89. });
  90. }
  91. function handleBrowserRequest(
  92. request: Request,
  93. responseStatusCode: number,
  94. responseHeaders: Headers,
  95. remixContext: EntryContext
  96. ) {
  97. return new Promise((resolve, reject) => {
  98. let shellRendered = false;
  99. const { pipe, abort } = renderToPipeableStream(
  100. <RemixServer
  101. context={remixContext}
  102. url={request.url}
  103. abortDelay={ABORT_DELAY}
  104. />,
  105. {
  106. onShellReady() {
  107. shellRendered = true;
  108. const body = new PassThrough();
  109. const stream = createReadableStreamFromReadable(body);
  110. responseHeaders.set("Content-Type", "text/html");
  111. resolve(
  112. new Response(stream, {
  113. headers: responseHeaders,
  114. status: responseStatusCode,
  115. })
  116. );
  117. pipe(body);
  118. },
  119. onShellError(error: unknown) {
  120. reject(error);
  121. },
  122. onError(error: unknown) {
  123. responseStatusCode = 500;
  124. // Log streaming rendering errors from inside the shell. Don't log
  125. // errors encountered during initial shell rendering since they'll
  126. // reject and get logged in handleDocumentRequest.
  127. if (shellRendered) {
  128. console.error(error);
  129. }
  130. },
  131. }
  132. );
  133. setTimeout(abort, ABORT_DELAY);
  134. });
  135. }