Skip to content

Recipes

m4bwav edited this page Sep 29, 2026 · 1 revision

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.

Save a post to a file

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).

Decode HTML entities

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 = {'&quot;': '"', '&#39;': "'", '&lt;': '<', '&gt;': '>', '&amp;': '&'};
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.

Several posts

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.

Handle throttling

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.

Give up after a while

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.

Wrap the callback form in a Promise

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.

Behind a proxy

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=1 with HTTPS_PROXY before 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 setting process.env inside the program is too late.

  • Node 20.20.2: NODE_USE_ENV_PROXY does nothing. Install undici (npm install undici@7) and preload a two-line file with node --require ./proxy.cjs app.mjs:

    const {setGlobalDispatcher, EnvHttpProxyAgent} = require('undici');
    setGlobalDispatcher(new EnvHttpProxyAgent());
  • Deno 2.9.6 and Bun 1.4.2 read HTTPS_PROXY themselves.

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.

From a shell script

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.md

prints (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).

Clone this wiki locally