Repository navigation
Recipes
Tasks that come up with the package, each run as shown against the published 2.0.0 and the local stand-in for the API. Outputs that show a post are sample content made up for this wiki.
import {writeFile, readFile} from 'node:fs/promises';
import {retrieveMarkdown} from 'stack-exchange-markdown-retriever';
const markdown = await retrieveMarkdown({site: 'scifi', entityId: 127968});
if (markdown === null) {
console.log('no such post');
} else {
await writeFile('127968.md', markdown);
console.log((await readFile('127968.md', 'utf8')).split('\n')[0]);
}It prints the file's first line (sample content):
Sample question body, made up for this wiki (not the real post).
The API returns body_markdown with HTML entities such as " in it, and the package passes them on. To turn the common ones back into characters:
import {retrieveMarkdown} from 'stack-exchange-markdown-retriever';
const entities = {'"': '"', ''': "'", '<': '<', '>': '>', '&': '&'};
const markdown = await retrieveMarkdown({site: 'scifi', entityId: 127968});
console.log(markdown.replaceAll(/&(?:quot|#39|lt|gt|amp);/g, entity => entities[entity]));prints (sample content):
Sample question body, made up for this wiki (not the real post).
It keeps HTML entities: "quoted" & more.
an indented code line
Which entities the real API writes was not checked for this wiki; a full HTML entity decoder covers the rest.
One call fetches one post. Call it once per id, one after the other, and each call counts against the API's daily quota:
import {retrieveMarkdown} from 'stack-exchange-markdown-retriever';
for (const entityId of [1, 2, 404]) {
const markdown = await retrieveMarkdown({entityId});
console.log(entityId, markdown);
}prints (sample content; 404 has no post):
1 Sample question one.
2 Sample question two.
404 null
Ids joined by ; ('1;2') travel in one request, but the call returns only the first post the API lists.
The API answers an over-quota client with an error whose error_name is throttle_violation; the package turns it into a StackExchangeError with errorName set:
import {retrieveMarkdown, StackExchangeError} from 'stack-exchange-markdown-retriever';
try {
await retrieveMarkdown({entityId: 9002});
} catch (error) {
if (error instanceof StackExchangeError && error.errorName === 'throttle_violation') {
console.log('throttled:', error.message);
} else {
throw error;
}
}With the stand-in's throttling route (id 9002) it prints:
throttled: too many requests from this IP, more requests available in 3600 seconds
The package does not wait or retry. An API key (apiKey) raises the quota.
The timeout option (default 30 seconds) or any AbortSignal ends a call:
await retrieveMarkdown({entityId: 9011, signal: AbortSignal.timeout(300)});rejects, against a stand-in route that never answers, with:
TimeoutError: The operation was aborted due to timeout
The messages for each way of stopping are on Edge cases and errors.
Without a callback the function already returns a Promise. Code that must keep the callback form can wrap it by hand, minding the (markdown, error) order:
const {retrieveMarkdown} = require('stack-exchange-markdown-retriever');
// The callback form, wrapped by hand: note the (markdown, error) order.
const get = options => new Promise((resolve, reject) => {
retrieveMarkdown(options, (markdown, error) => (error ? reject(error) : resolve(markdown)));
});
get({entityId: 1010, isForAnswer: true}).then(markdown => console.log(markdown));prints (sample content):
Sample answer, made up for this wiki, with a [link](https://example.com/).
util.promisify expects an error-first callback, so it gets the order backwards and rejects with the markdown:
const {promisify} = require('node:util');
const {retrieveMarkdown} = require('stack-exchange-markdown-retriever');
promisify(retrieveMarkdown)({entityId: 1}).then(
markdown => console.log('resolved', markdown),
error => console.log('rejected with', error),
);prints (sample content):
rejected with Sample question one.
The package reads no proxy settings; the runtime's fetch does. What worked in this wiki's runs, with a proxy on 127.0.0.1 standing in for yours:
-
Node 24.18.0: set
NODE_USE_ENV_PROXY=1withHTTPS_PROXYbefore Node starts:NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://proxy.example:8080 node app.mjs. The same applies to the command line tool. Node reads both at startup, so settingprocess.envinside the program is too late. -
Node 20.20.2:
NODE_USE_ENV_PROXYdoes nothing. Install undici (npm install undici@7) and preload a two-line file withnode --require ./proxy.cjs app.mjs:const {setGlobalDispatcher, EnvHttpProxyAgent} = require('undici'); setGlobalDispatcher(new EnvHttpProxyAgent());
-
Deno 2.9.6 and Bun 1.4.2 read
HTTPS_PROXYthemselves.
NO_PROXY lists hosts that skip the proxy. A proxy that intercepts TLS needs its certificate trusted: NODE_EXTRA_CA_CERTS=<file> for Node and Bun, DENO_CERT=<file> for Deno. The runs above used exactly these variables. What happens when the proxy drops the tunnel is on Edge cases and errors.
Save a few questions to files with the command line tool:
for id in 1 2; do stack-exchange-markdown-retriever "$id" > "question-$id.md"; done
head question-1.md question-2.mdprints (sample content):
==> question-1.md <==
Sample question one.
==> question-2.md <==
Sample question two.
A missing post writes null to its file and still exits 0, so check the content, not only the exit code (Commands).
This wiki describes stack-exchange-markdown-retriever 2.0.0 and was last updated on 2026-09-29. The library is MIT licensed. Report problems in the issues. On npm: stack-exchange-markdown-retriever.
Using it
The releases
Contributing
Elsewhere