• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage / 29922382158

22 Jul 2026 01:05PM UTC coverage: 80.152% (+0.3%) from 79.825%
29922382158

push

github

web-flow
fix: replace xmljs with fxp and hoist builder (#1253)

Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>

5642 of 7585 branches covered (74.38%)

Branch coverage included in aggregate %.

102 of 104 new or added lines in 3 files covered. (98.08%)

10757 of 12875 relevant lines covered (83.55%)

413.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.12
/src/http/plugins/xml.ts
1
import accepts from '@fastify/accepts'
2
import { ERRORS } from '@internal/errors'
3
import Builder from 'fast-xml-builder'
4
import { XMLParser } from 'fast-xml-parser'
5
import type { FastifyInstance, FastifyRequest } from 'fastify'
6
import fastifyPlugin from 'fastify-plugin'
7

8
type XmlParserOptions = {
9
  disableContentParser?: boolean
10
  parseAsArray?: string[]
11
  responseNamespace?: string
12
}
13

14
const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
39✔
15
const XML_NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*$/
39✔
16
const XML_ENTITY_REFERENCE = /&([^;&]*);|&/g
39✔
17
const XML_NUMERIC_ENTITY = /^#(?:x([\dA-F]+)|(\d+))$/i
39✔
18
const XML_ENTITIES: Record<string, string> = {
39✔
19
  amp: '&',
20
  apos: "'",
21
  gt: '>',
22
  lt: '<',
23
  quot: '"',
24
}
25
const XML_CHARACTER_RANGE = String.raw`\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}`
39✔
26
// biome-ignore lint/suspicious/noControlCharactersInRegex: XML 1.0 excludes these ranges.
27
const INVALID_XML_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/
39✔
28
const XML_ESCAPED_CHARACTER = new RegExp(`[&<>\\t\\n\\r]|[^${XML_CHARACTER_RANGE}]`, 'gu')
39✔
29
const HAS_XML_ESCAPED_CHARACTER = new RegExp(XML_ESCAPED_CHARACTER.source, 'u')
39✔
30
const XML_ESCAPES: Record<string, string> = {
39✔
31
  '&': '&amp;',
32
  '<': '&lt;',
33
  '>': '&gt;',
34
  '\t': '&#x9;',
35
  '\n': '&#xA;',
36
  '\r': '&#xD;',
37
}
38

39
const escapeXmlValue = (value: unknown, attribute: boolean) => {
39✔
40
  const string = typeof value === 'string' ? value : String(value)
1,253✔
41
  if (!HAS_XML_ESCAPED_CHARACTER.test(string)) return string
1,253✔
42

43
  return string.replace(XML_ESCAPED_CHARACTER, (character) =>
32✔
44
    !attribute && (character === '\t' || character === '\n')
2,321!
45
      ? character
46
      : (XML_ESCAPES[character] ?? '\uFFFD')
2,341✔
47
  )
48
}
49
const serializeXmlText = (_tagName: string, value: unknown) =>
39✔
50
  escapeXmlValue(value instanceof Date ? value.toISOString() : value, false)
1,233✔
51
const serializeXmlAttribute = (_name: string, value: unknown) => escapeXmlValue(value, true)
48✔
52

53
// Escapes a string for use inside a double-quoted XML attribute value.
54
export function escapeXmlAttribute(value: string): string {
55
  return value
3,181✔
56
    .replace(/&/g, '&amp;')
57
    .replace(/"/g, '&quot;')
58
    .replace(/</g, '&lt;')
59
    .replace(/>/g, '&gt;')
60
}
61

62
/**
63
 * Inserts a precomputed ` xmlns="..."` attribute into the root tag of an
64
 * already-built XML document. The scan is quote-aware and stops at the end of
65
 * the opening root tag, so its cost is independent from the response body size.
66
 */
67
export function insertRootNamespace(xmlPayload: string, namespaceAttribute: string): string {
68
  const declarationEnd = xmlPayload.indexOf('?>')
171✔
69
  const rootStart = declarationEnd === -1 ? 0 : declarationEnd + 2
171!
70

71
  let quote = 0
171✔
72
  for (let index = rootStart; index < xmlPayload.length; index++) {
171✔
73
    const character = xmlPayload.charCodeAt(index)
3,369✔
74
    if (quote !== 0) {
3,369✔
75
      if (character === quote) {
31✔
76
        quote = 0
3✔
77
      }
78
      continue
31✔
79
    }
80

81
    if (character === 34 || character === 39) {
3,338✔
82
      quote = character
3✔
83
      continue
3✔
84
    }
85

86
    if (character === 32 && xmlPayload.startsWith('xmlns=', index + 1)) {
3,335✔
87
      return xmlPayload
1✔
88
    }
89

90
    if (character === 62) {
3,334✔
91
      const tagEnd = xmlPayload.charCodeAt(index - 1) === 47 ? index - 1 : index
170✔
92
      return xmlPayload.slice(0, tagEnd) + namespaceAttribute + xmlPayload.slice(tagEnd)
170✔
93
    }
94
  }
95

NEW
96
  return xmlPayload
×
97
}
98

99
function isValidXmlCodePoint(codePoint: number) {
100
  return (
11✔
101
    codePoint === 0x9 ||
67✔
102
    codePoint === 0xa ||
103
    codePoint === 0xd ||
104
    (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
105
    (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
106
    (codePoint >= 0x10000 && codePoint <= 0x10ffff)
107
  )
108
}
109

110
const xmlEntityDecoder = {
39✔
111
  setExternalEntities() {},
112
  addInputEntities() {
113
    throw new Error('DOCTYPE is not supported')
2✔
114
  },
115
  reset() {},
116
  setXmlVersion() {},
117
  decode(text: string) {
118
    if (text.indexOf('&') === -1) return text
160✔
119

120
    return text.replace(XML_ENTITY_REFERENCE, (_reference, entity: string | undefined) => {
24✔
121
      if (entity === undefined) throw new Error('Unescaped ampersand')
38✔
122

123
      if (Object.hasOwn(XML_ENTITIES, entity)) {
36✔
124
        return XML_ENTITIES[entity]
25✔
125
      }
126

127
      const match = XML_NUMERIC_ENTITY.exec(entity)
11✔
128
      const codePoint = match
11✔
129
        ? Number.parseInt(match[1] ?? match[2], match[1] === undefined ? 10 : 16)
21✔
130
        : Number.NaN
131

132
      if (!isValidXmlCodePoint(codePoint)) {
38✔
133
        const message = entity.startsWith('#')
6✔
134
          ? 'Invalid numeric character reference'
135
          : 'Undeclared XML entity'
136
        throw new Error(message)
6✔
137
      }
138

139
      return String.fromCodePoint(codePoint)
5✔
140
    })
141
  },
142
}
143

144
const xmlBuilder = new Builder({
39✔
145
  format: false,
146
  ignoreAttributes: false,
147
  attributeNamePrefix: '',
148
  attributesGroupName: '$',
149
  textNodeName: '_',
150
  suppressEmptyNode: true,
151
  suppressBooleanAttributes: false,
152
  processEntities: false,
153
  tagValueProcessor: serializeXmlText,
154
  attributeValueProcessor: serializeXmlAttribute,
155
})
156

157
const acceptedTypes = ['application/xml', 'text/xml', 'text/html']
39✔
158
const buildXml = (payload: unknown) => XML_DECLARATION + xmlBuilder.build(payload)
175✔
159

160
function acceptsXmlResponse(req: FastifyRequest): boolean {
161
  const accept = req.headers.accept
197✔
162
  if (accept === undefined || accept === '*/*' || acceptedTypes.includes(accept)) {
197✔
163
    return true
179✔
164
  }
165

166
  const normalizedAccept = accept.trim().toLowerCase()
18✔
167
  if (
18✔
168
    normalizedAccept !== accept &&
22✔
169
    (normalizedAccept === '*/*' || acceptedTypes.includes(normalizedAccept))
170
  ) {
171
    return true
2✔
172
  }
173

174
  return req.accepts().types(acceptedTypes) !== false
16✔
175
}
176

177
function isXmlDocumentPayload(payload: unknown): payload is Record<string, unknown> {
178
  if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
217!
NEW
179
    return false
×
180
  }
181

182
  const rootKeys = Object.keys(payload)
217✔
183
  if (rootKeys.length !== 1 || !XML_NAME.test(rootKeys[0])) {
217✔
184
    return false
5✔
185
  }
186

187
  const rootValue = (payload as Record<string, unknown>)[rootKeys[0]]
212✔
188
  return rootValue !== undefined && !Array.isArray(rootValue)
212✔
189
}
190

191
export const xmlParser = fastifyPlugin(
39✔
192
  async function (fastify: FastifyInstance, opts: XmlParserOptions) {
193
    fastify.register(accepts)
3,224✔
194

195
    if (!opts.disableContentParser) {
3,224✔
196
      const arrayPaths = new Set(opts.parseAsArray?.filter(Boolean))
2,935✔
197
      const arrayTags = new Set(
2,935✔
198
        [...arrayPaths].map((path) => path.slice(path.lastIndexOf('.') + 1))
580✔
199
      )
200
      const parser = new XMLParser({
2,935✔
201
        ignoreDeclaration: true,
202
        ignoreAttributes: false,
203
        attributeNamePrefix: '',
204
        attributesGroupName: '$',
205
        textNodeName: '_',
206
        parseTagValue: false,
207
        trimValues: false,
208
        ignorePiTags: true,
209
        jPath: false,
210
        entityDecoder: xmlEntityDecoder,
211
        tagValueProcessor: (_tagName, value, _jPath, _hasAttributes, isLeafNode) =>
212
          !isLeafNode && value.trim() === '' ? '' : undefined,
98✔
213
        isArray: (tagName, path) => arrayTags.has(tagName) && arrayPaths.has(String(path)),
361✔
214
      })
215

216
      fastify.addContentTypeParser(
2,935✔
217
        ['text/xml', 'application/xml'],
218
        { parseAs: 'string' },
219
        (_request, body, done) => {
220
          if (!body) {
153✔
221
            done(null, null)
100✔
222
            return
100✔
223
          }
224

225
          try {
53✔
226
            const raw = body.toString()
53✔
227
            const xml = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw
53✔
228
            if (INVALID_XML_CHARACTER.test(xml) || !xml.isWellFormed()) {
153✔
229
              throw new Error('Invalid XML character')
1✔
230
            }
231
            const payload = parser.parse(xml, true)
52✔
232
            if (!isXmlDocumentPayload(payload)) {
52✔
233
              throw new Error('XML payload must contain a single root element')
2✔
234
            }
235
            done(null, payload)
33✔
236
          } catch (error) {
237
            const message = error instanceof Error ? error.message : String(error)
20!
238
            done(
20✔
239
              Object.assign(ERRORS.InvalidRequest(`Invalid XML payload: ${message}`), {
240
                statusCode: 400,
241
              })
242
            )
243
          }
244
        }
245
      )
246
    }
247

248
    const namespaceAttribute = opts.responseNamespace
3,224✔
249
      ? ` xmlns="${escapeXmlAttribute(opts.responseNamespace)}"`
250
      : undefined
251
    const serializeXml = namespaceAttribute
3,224✔
252
      ? (payload: unknown) => insertRootNamespace(buildXml(payload), namespaceAttribute)
164✔
253
      : buildXml
254

255
    fastify.addHook('preSerialization', (req, res, payload, done) => {
3,224✔
256
      if (acceptsXmlResponse(req)) {
197✔
257
        if (!isXmlDocumentPayload(payload)) {
182✔
258
          done(
7✔
259
            ERRORS.InternalError(
260
              undefined,
261
              'XML response payload must be an object with a single root element'
262
            )
263
          )
264
          return
7✔
265
        }
266

267
        res.header('content-type', 'application/xml; charset=utf-8')
175✔
268
        res.serializer(serializeXml)
175✔
269
      }
270

271
      done(null, payload)
190✔
272
    })
273
  },
274
  { name: 'xml-parser' }
275
)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc