fix: prevent 413 errors and navigation crashes from large embedded images

This commit is contained in:
Erza Bytyci
2026-08-31 21:29:02 +00:00
parent acf75eb8c9
commit bbf9c3754a
6 changed files with 102 additions and 20 deletions
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
"use client";
export default function Error({ error, reset }) {
return (
<div>
<h2>Etwas ist schiefgelaufen / Something went wrong</h2>
<p>
Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.
</p>
<button onClick={() => reset()}>Erneut versuchen / Try again</button>
</div>
);
}
+10 -2
View File
@@ -45,13 +45,21 @@ export default async function Chapters({ params }) {
<Row pb="xl" className="chapter-nav-row" justify="space-between">
<Container className="chapter-nav-left">
{previousLink && <LinkButton href={previousLink}>Zurück</LinkButton>}
{previousLink && (
<LinkButton href={previousLink} hardNavigation>
Zurück
</LinkButton>
)}
</Container>
<Container className="chapter-nav-right">
{showLearningFormLink && (
<LinkButton href="/learning">Go to form</LinkButton>
)}
{nextLink && <LinkButton href={nextLink}>Weiter</LinkButton>}
{nextLink && (
<LinkButton href={nextLink} hardNavigation>
Weiter
</LinkButton>
)}
</Container>
</Row>
</Column>
@@ -134,6 +134,40 @@ function setHtmlContent(quill, html) {
quill.clipboard.dangerouslyPasteHTML(0, content, "api");
}
const MAX_IMAGE_DIMENSION = 1600;
const IMAGE_COMPRESSION_QUALITY = 0.75;
// Downscales/re-encodes images client-side so pasted base64 stays well under proxy body-size limits.
function compressImageFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error);
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error("Could not read image"));
img.onload = () => {
const scale = Math.min(
1,
MAX_IMAGE_DIMENSION / Math.max(img.width, img.height)
);
const width = Math.round(img.width * scale);
const height = Math.round(img.height * scale);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(img, 0, 0, width, height);
const outputType =
file.type === "image/png" ? "image/png" : "image/jpeg";
resolve(canvas.toDataURL(outputType, IMAGE_COMPRESSION_QUALITY));
};
img.src = reader.result;
};
reader.readAsDataURL(file);
});
}
function isNodeEffectivelyEmptyText(node) {
if (!(node instanceof HTMLElement)) {
return false;
@@ -492,8 +526,34 @@ const ClientEditor = (props) => {
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
];
const imageHandler = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.onchange = async () => {
const file = input.files?.[0];
if (!file) {
return;
}
try {
const dataUrl = await compressImageFile(file);
const range = quill.getSelection(true);
quill.insertEmbed(range.index, "image", dataUrl, "user");
quill.setSelection(range.index + 1, 0, "silent");
} catch (error) {
console.warn("Image compression failed", error);
}
};
input.click();
};
const modules = {
toolbar: toolbarOptions,
toolbar: {
container: toolbarOptions,
handlers: {
image: imageHandler,
},
},
table: false,
"table-better": {
language: "en_US",
+15 -5
View File
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import styles from "@/components/ui/button/button.module.css";
import { withBasePath } from "@/shared/utils/basePath";
export const LinkButton = ({
href,
@@ -9,6 +10,7 @@ export const LinkButton = ({
size = "md",
width = "fit",
className,
hardNavigation = false,
}) => {
const classes = [
styles.baseButton,
@@ -19,13 +21,21 @@ export const LinkButton = ({
]
.filter(Boolean)
.join(" ");
const style = { display: "inline-block", textDecoration: "none" };
// Bypass Next.js soft-navigation (RSC fetch) for pages whose HTML may be too
// large to stream reliably, avoiding client-side crashes on partial responses.
// next/link auto-prefixes basePath; a plain <a> does not, so add it manually.
if (hardNavigation) {
return (
<a href={withBasePath(href)} className={classes} style={style}>
{children}
</a>
);
}
return (
<Link
href={href}
className={classes}
style={{ display: "inline-block", textDecoration: "none" }}
>
<Link href={href} className={classes} style={style}>
{children}
</Link>
);
+2 -11
View File
@@ -1,13 +1,4 @@
{
"status": "failed",
"failedTests": [
"e05c51b8ad1b3eb66010-0e43830ad8f2dfd8d1e2",
"c5b6e4261f23dff4104e-cb5330f948ccf9c8b61e",
"c5b6e4261f23dff4104e-fd730098ea5d4ab30d2a",
"c5b6e4261f23dff4104e-4c06fad2b2a291767cec",
"18e1443b7169cebccd21-4a0da125f055d790cf05",
"cf4ffee829137744e3cd-e6406448e73a28c0ddd9",
"d936625e1207b3e19d4b-67eb4c2dd9e8efb88e3d",
"d8918302922cfb4b5ab2-53975d1f9ee0854667fa"
]
"status": "passed",
"failedTests": []
}