I’m building a small React,TypeScript,Axios app for encrypted notes.
-
Frontend: React (with Axios)
-
Backend: Node/Express on http://localhost:3000
-
Tunnel: ngrok (ngrok http 3000)
Everything works fine locally, but when I use ngrok, some requests return an HTML page from ngrok instead of the JSON response from my backend.
src/api/notes.ts
import axios from "axios";
const api = axios.create({
baseURL: "https://efaed12151ea.ngrok-free.app",
headers: {
"Content-Type": "application/json",
},
});
export type NotePayload = {
encryptedContent: string;
encryptedIv: string;
};
export type CreateNoteResponse = {
data: {
id: string;
};
};
export type GetNoteResponse = {
data: {
id: string;
encryptedContent: string;
encryptedIv: string;
};
};
export async function createNote(payload: NotePayload): Promise<string> {
const response = await api.post<CreateNoteResponse>("/notes", payload);
return response.data.data.id;
}
export async function getNote(id: string): Promise<GetNoteResponse["data"]> {
const response = await api.get<GetNoteResponse>(`/notes/${id}`);
console.log(response);
return response.data.data;
}
src/pages/CreateNote.tsx
import React, { useState } from "react";
import { encrypt, generateShortKey } from "../utils/crypto";
import { createNote } from "../api/notes";
import NoteForm from "../components/NoteForm";
import NoteResult from "../components/NoteResult";
export default function CreateNote() {
const [content, setContent] = useState("");
const [shortKey, setShortKey] = useState<string | null>(null);
const [link, setLink] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) {
setError("Note content cannot be empty.");
return;
}
setError(null);
setLoading(true);
try {
const key = generateShortKey();
const { ciphertext, iv } = await encrypt(content, key);
const id = await createNote({
encryptedContent: ciphertext,
encryptedIv: iv,
});
setShortKey(key);
setLink(`${window.location.origin}/${id}#${key}`);
} catch (err: any) {
console.error("Failed to create note:", err);
setError("Failed to create note. Please try again.");
} finally {
setLoading(false);
}
}
const handleReset = () => {
setContent("");
setShortKey(null);
setLink(null);
setError(null);
};
const copyLink = async () => {
if (link) {
try {
await navigator.clipboard.writeText(link);
alert("Link copy!");
} catch (err) {
console.error("Failed to copy link:", err);
}
}
};
return (
<div className="p-6 mx-auto my-8 max-w-lg border rounded-lg shadow-lg bg-white">
<NoteForm
content={content}
onChange={setContent}
onSubmit={handleSubmit}
loading={loading}
error={error}
/>
{link && (
<NoteResult link={link} onCopy={copyLink} onReset={handleReset} />
)}
</div>
);
}
src/pages/ViewNote.tsx (simplified)
useEffect(() => {
const shortKey = window.location.hash.slice(1);
if (!id || !shortKey) {
navigate("/404");
return;
}
const load = async () => {
try {
const { encryptedContent, encryptedIv } = await getNote(id);
const decrypted = await decrypt(encryptedContent, encryptedIv, shortKey);
setNote(decrypted);
} catch (err) {
console.error("Error fetching note:", err);
navigate("/404");
} finally {
setLoading(false);
}
};
load();
}, [id]);
Console.log(response) write FAIL:
{data: '<!DOCTYPE html>n<html class="h-full" lang="en-US" …EiLCJ0aXRsZSI6Ik9LIn0="></div>n </body>n</html>n', status: 200, statusText: '', headers: AxiosHeaders, config: {…}, …} config : {transitional: {…}, adapter: Array(3), transformRequest: Array(1), transformResponse: Array(1), timeout: 0, …} data : "<!DOCTYPE html>n<html class="h-full" lang="en-US" dir="ltr">n <head>n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/euclid-square/EuclidSquare-Regular-WebS.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/euclid-square/EuclidSquare-RegularItalic-WebS.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/euclid-square/EuclidSquare-Medium-WebS.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/euclid-square/EuclidSquare-Semibold-WebS.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/euclid-square/EuclidSquare-MediumItalic-WebS.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/ibm-plex-mono/IBMPlexMono-Text.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/ibm-plex-mono/IBMPlexMono-TextItalic.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/ibm-plex-mono/IBMPlexMono-SemiBold.woff" as="font" type="font/woff" crossorigin="anonymous" />n <link rel="preload" href="https://cdn.ngrok.com/static/fonts/ibm-plex-mono/IBMPlexMono-SemiBoldItalic.woff" as="font" type="font/woff" crossorigin="anonymous" />n <meta charset="utf-8">n <meta name="author" content="ngrok">n <meta name="description" content="ngrok is the fastest way to put anything on the internet with a single command.">n <meta name="robots" content="noindex, nofollow">n <meta name="viewport" content="width=device-width, initial-scale=1">n <link id="style" rel="stylesheet" href="https://cdn.ngrok.com/static/css/error.css">n <noscript>You are about to visit efaed12151ea.ngrok-free.app, served by xx.xxx.xx.xxx. This website is served for free through ngrok.com. You should only visit this website if you trust whoever sent the link to you. (ERR_NGROK_6024)</noscript>n <script id="script" src="https://cdn.ngrok.com/static/js/error.js" type="text/javascript"></script>n </head>n <body class="h-full" id="ngrok">n <div id="root" data-payload="eyJjZG5CYXNlIjoiaHR0cHM6Ly9jZG4ubmdyb2suY29tLyIsImNvZGUiOiI2MDI0IiwiaG9zdHBvcnQiOiJlZmFlZDEyMTUxZWEubmdyb2stZnJlZS5hcHAiLCJtZXNzYWdlIjoiWW91IGFyZSBhYm91dCB0byB2aXNpdCBlZmFlZDEyMTUxZWEubmdyb2stZnJlZS5hcHAsIHNlcnZlZCBieSA5My4xNTkuNDYuMTMxLiBUaGlzIHdlYnNpdGUgaXMgc2VydmVkIGZvciBmcmVlIHRocm91Z2ggbmdyb2suY29tLiBZb3Ugc2hvdWxkIG9ubHkgdmlzaXQgdGhpcyB3ZWJzaXRlIGlmIHlvdSB0cnVzdCB3aG9ldmVyIHNlbnQgdGhlIGxpbmsgdG8geW91LiIsInNlcnZpbmdJUCI6IjkzLjE1OS40Ni4xMzEiLCJ0aXRsZSI6Ik9LIn0="></div>n </body>n</html>n" headers : AxiosHeaders {content-length: '2878', content-type: 'text/html'} request : XMLHttpRequest {onreadystatechange: null, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …} status : 200 statusText : "" [[Prototype]] : Object
What works
{
"id": "h7KH5MjnxY7",
"encryptedContent": "...",
"encryptedIv": "..."
}
What doesn’t work
GET https://efaed12151ea.ngrok-free.app/notes/:id → sometimes returns JSON, but returns an HTML page from ngrok instead.
Example log from console.log(response) in Axios:
{
"status": 200,
"headers": { "content-type": "text/html" },
"data": "<!DOCTYPE html><html ... (ERR_NGROK_6024) ... </html>"
}
So Axios sees a 200 OK, but the response is an HTML warning page from ngrok, not JSON.
What I tried
Manually checked in browser:
-
https://efaed12151ea.ngrok-free.app/notes/:id → often HTML (ngrok error page)
Curl tests:
curl -i https://efaed12151ea.ngrok-free.app/notes/h7KH5MjnxY7 # returns HTML with ERR_NGROK_6024
-
Tried with /api/notes/:id as well — same behavior.
Why does ngrok return its own HTML page (ERR_NGROK_6024) instead of forwarding my backend’s JSON response?
-
Is this a limitation of ngrok free tunnels (idle timeout, etc.)?
-
Do I need to change how Axios calls the API (baseURL, headers, etc.)?
-
Is there a reliable way to ensure my frontend always gets JSON from ngrok?
But when I open "/ID#shortKey", instead of JSON I get an ngrok HTML page.
In console.log(response) I see status: 200, but response.data contains an HTML string (<!DOCTYPE html> ...), not JSON.
I changed some variables, also rewrote the code itself more than once. Output in the console.log id, shortkey, etc.
The backend is fine. Now I can’t get NGROK to work. Sorry if it’s unclear, this is the first time I’ve asked for help on this site.