Skip to content

How Markdown Is Retrieved

m4bwav edited this page Sep 29, 2026 · 1 revision

One call makes one GET request to the Stack Exchange API, version 2.2, and hands back the first item's body_markdown. This page gives the exact request for each option, what comes back for each kind of answer, and when the callback runs. Every request line below is what the local stand-in for api.stackexchange.com received from the published 2.0.0.

The request

The URL is always:

https://api.stackexchange.com/2.2/<questions or answers>/<ids>?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY[&key=<apiKey>]&site=<site>

The parameters come in that order. filter is a fixed API filter that makes the API include body_markdown; nothing else in the URL can be changed, including the API version.

{entityId: 11227809} requests:

GET https://api.stackexchange.com/2.2/questions/11227809?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow

{entityId: 1010, isForAnswer: true} requests:

GET https://api.stackexchange.com/2.2/answers/1010?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow

site goes into the query as given, short name or domain. {entityId: 127968, site: 'scifi.stackexchange.com'} requests:

GET https://api.stackexchange.com/2.2/questions/127968?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=scifi.stackexchange.com

apiKey becomes key, placed before site and escaped. {entityId: 1, apiKey: 'k=y&z w/+'} requests:

GET https://api.stackexchange.com/2.2/questions/1?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&key=k%3Dy%26z%20w%2F%2B&site=stackoverflow

An empty apiKey is left out, and an empty site becomes stackoverflow. An apostrophe is escaped as %27 (1.x did the same), so site: "it's" requests:

GET https://api.stackexchange.com/2.2/questions/1?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=it%27s

site and apiKey values that are not strings are written the way 1.x wrote them: a number as its digits (site=123), an object as nothing (site=).

Ids

The id goes into the path as written, so a string keeps its leading zeros. {entityId: '011227809'} requests:

GET https://api.stackexchange.com/2.2/questions/011227809?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow

Numbers are written with String(), so 2 ** 53 becomes 9007199254740992, while 1e21 (whose string is 1e+21) is refused. Several ids joined by ; go into one request and the call returns the first item in the API's answer. With the stand-in, which lists items in the order asked, {entityId: '2;1'} requested:

GET https://api.stackexchange.com/2.2/questions/2;1?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow

and resolved to the text of question 2. The real API's item order for such a request was not tested; the package sends order=asc and takes item 0. Ids that are not post ids are refused before any request: see Edge cases and errors.

What comes back

The package parses the JSON and returns items[0].body_markdown. It does nothing to the text: no rendering, no trimming, and HTML entities such as &quot; stay as the API sent them. What each kind of answer gives, with the callback and without one:

The API's answer Callback gets Promise gives
a post with body_markdown the text the text
no items (no such post) (null, null) null
a post without body_markdown (undefined, null) null
an empty body_markdown ('', null) ''
body_markdown that is a number (42) (42, null) null
the JSON literal null (null, null) null
an error body without error_message (status 404) (null, null) null
an error_message (any status, even 200 with items) (null, StackExchangeError) rejects
not JSON (null, StackExchangeError) rejects

The HTTP status alone decides nothing: a 404 without error_message resolves null, and a 200 with error_message fails. From the verification output, a post without body_markdown:

callback: ("<undefined>", null)
promise: resolves null

and an error body without error_message, status 404:

callback: (null, null)
promise: resolves null

Compressed bodies

The API compresses its answers. fetch undoes a Content-Encoding header itself; the package then also looks at the first bytes of the body and inflates gzip or zlib data that arrived without the header, as 1.x's zlib.unzip did. So all of these gave their text: gzip with the header, deflate with the header, gzip bytes without a header, and plain JSON (which 1.x could not read).

A body the package inflates itself stops at 64 MiB with StackExchangeError: The decompressed response is larger than 67108864 bytes. A body with Content-Encoding: gzip is inflated by fetch before the package sees it, so that limit does not apply to it: 65 MiB of valid JSON sent that way came back whole. The verification output for that case:

resolves a string of 68157440 characters

Redirects

fetch follows redirects. A 302 from the stand-in to /2.2/questions/1 gave question 1's text after two requests.

When the callback runs

The callback always runs after retrieveMarkdown returns, never during the call:

import {retrieveMarkdown} from 'stack-exchange-markdown-retriever';

retrieveMarkdown({entityId: 1}, (markdown, error) => {
  console.log('callback:', markdown, error);
});
console.log('retrieveMarkdown returned');

prints (sample content):

retrieveMarkdown returned
callback: Sample question one. null

It runs once, in a microtask of its own, outside the Promise chain. That is why an exception thrown in it is uncaught and why it is not called a second time; Edge cases and errors shows the result.

What the package does not do

  • It keeps no cache and makes one request per call, even for the same id twice.
  • It does not track the API's quota (300 requests a day per IP address without a key, as the README says) and does not back off or retry. A throttled request fails with the API's error: see Recipes.
  • It reads no environment variables. Proxies are the runtime's business: see Recipes.

Clone this wiki locally