nk.httpRequest sending a malformed x-www-form-urlencoded request

Hello guys!

Currently Im trying to do an Discord OAuth Authentication for my game dashboard so the user can link his Discord Account in the game but im facing some issues when doing the following call via nk.httpRequest:

https://discord.com/api/oauth2/token

It appears to return the error indicated by discord documentation:

In accordance with the relevant RFCs, the token and token revocation URLs will only accept a content type of application/x-www-form-urlencoded. JSON content is not permitted and will return an error.

The current nk.httpRequest implementation seems to still send a JSON Body when doing a
‘Content-Type’: ‘application/x-www-form-urlencoded’ request

  1. Versions: Nakama {3.21.0}, {Docker}, {Nakama Common 1.31.0}
  2. Server Framework Runtime language {TS/JS}

This is my implementation:

export function connectDiscordRpc(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string {
	let parsedPayload = JSON.parse(payload) as DiscordAuthPayload

	const discordClientId = ctx.env.discordClientId
	const discordClientSecretId = ctx.env.discordClientSecretId
	const discordRedirectUrl = ctx.env.discordRedirectUrl

	if (
		!discordClientId ||
		!discordClientSecretId ||
		!discordRedirectUrl ||
		discordClientId === '' ||
		discordClientSecretId === '' ||
		discordRedirectUrl === ''
	) {
		throw FormatGRPCError('Discord environment variables not set', nkruntime.Codes.INTERNAL)
	}

	const body = {
		grant_type: 'authorization_code',
		code: parsedPayload.code,
		redirect_uri: discordRedirectUrl,
	}

	const authEncoded = nk.base64Encode(`${discordClientId}:${discordClientSecretId}`)

	logger.info('Discord Auth Body: %#v', body)

	const headers = {
		'Authorization': `Basic ${authEncoded}`,
		'Content-Type': 'application/x-www-form-urlencoded',
	}

	logger.info('Discord Headers: %#v', headers)

	const request = nk.httpRequest(discordAuth, 'post', headers, JSON.stringify(body))

	logger.info('Discord Auth Response: %#v', request)

	if (request.code !== 200) {
		throw FormatGRPCError('Failed to connect to Discord', nkruntime.Codes.PERMISSION_DENIED)
	}

	/// Rest of the code

	return JSON.stringify({ success: true })
}

this is the log:

{"msg":"Discord Auth: \"MTI...==\"","rpc_id":"connectdiscordrpc"}
{"msg":"Discord Auth Body: \"{\\\"grant_type\\\":\\\"authorization_code\\\",\\\"code\\\":\\\"9E12...\\\",\\\"redirect_uri\\\":\\\"https://testnet-...\\\"}\"","rpc_id":"connectdiscordrpc"}
{"msg":"Discord Auth Response: \"{\\\"code\\\":400,\\\"headers\\\":{\\\"Content-Security-Policy\\\":[\\\"frame-ancestors 'none'; default-src 'none'\\\"],\\\"Alt-Svc\\\":[\\\"h3=\\\\\\\":443\\\\\\\"; ma=86400\\\"],\\\"Cf-Ray\\\":[\\\"878ca3092fd35e19-MAD\\\"],\\\"X-Content-Type-Options\\\":[\\\"nosniff\\\"],\\\"Server\\\":[\\\"cloudflare\\\"],\\\"Content-Type\\\":[\\\"application/json\\\"],\\\"Nel\\\":[\\\"{\\\\\\\"success_fraction\\\\\\\":0,\\\\\\\"report_to\\\\\\\":\\\\\\\"cf-nel\\\\\\\",\\\\\\\"max_age\\\\\\\":604800}\\\"],\\\"Report-To\\\":[\\\"{\\\\\\\"endpoints\\\\\\\":[{\\\\\\\"url\\\\\\\":\\\\\\\"https:\\\\\\\\/\\\\\\\\/a.nel.cloudflare.com\\\\\\\\/report\\\\\\\\/v4?s=NqbhQ%%%\\\\\\\"}],\\\\\\\"group\\\\\\\":\\\\\\\"cf-nel\\\\\\\",\\\\\\\"max_age\\\\\\\":604800}\\\"],\\\"Via\\\":[\\\"1.1 google\\\"],\\\"Cf-Cache-Status\\\":[\\\"DYNAMIC\\\"],\\\"Content-Length\\\":[\\\"42\\\"],\\\"Set-Cookie\\\":[\\\"__dcfduid=; Expires=Sun, 22-Apr-2029 08:55:50 GMT; Max-Age=157680000; Secure; HttpOnly; Path=/; SameSite=Lax\\\",\\\"__sdcfduid=; Expires=Sun, 22-Apr-2029 08:55:50 GMT; Max-Age=157680000; Secure; HttpOnly; Path=/; SameSite=Lax\\\",\\\"__cfruid=-; path=/; domain=.discord.com; HttpOnly; Secure; SameSite=None\\\",\\\"_cfuvid=; path=/; domain=.discord.com; HttpOnly; Secure; SameSite=None\\\"],\\\"Strict-Transport-Security\\\":[\\\"max-age=31536000; includeSubDomains; preload\\\"],\\\"Date\\\":[\\\"Tue, 23 Apr 2024 08:55:50 GMT\\\"]},\\\"body\\\":\\\"{\\\\\\\"message\\\\\\\": \\\\\\\"400: Bad Request\\\\\\\", \\\\\\\"code\\\\\\\": 0}\\\"}\"","rpc_id":"connectdiscordrpc"}

This is a problem with the httpRequest or how Im formating the body to x-www-form-urlencoded?
When trying to do the same post call with postman it works.

Hello @igordias2,

If a body parameter is provided it’ll be sent as such, for application/x-www-form-urlencoded you should instead be able to pass them as query params encoded in the URL by building the string yourself

encodeURI(`${url}?grant_type=${grant_type}&code=${code}&redirect_uri=${redirect_uri}`)

and leave the body param empty.

Hope this helps.

1 Like

Sorry for marking as resolved I thought should be enough and probably is and Im doing something wrong but I was trying to do it here and it still has a problem. Now its not giving the error related to the body anymore (which is good) but an error related to the ‘code’.

Ive replaced all for the following

	const requestURL = encodeURI(`${discordAuth}?grant_type=${grant_type}&code=${parsedPayload.code}&redirect_uri=${discordRedirectUrl}`)

	// const requestURL = discordAuth + '?' + encodeURI(`code=${parsedPayload.code}&grant_type=${grant_type}&redirect_uri=${discordRedirectUrl}`)

	// const requestURL =
	// 	discordAuth +
	// 	`?grant_type=${encodeURIComponent(grant_type)}` +
	// 	`&code=${encodeURIComponent(parsedPayload.code)}` +
	// 	`&redirect_uri=${encodeURIComponent(discordRedirectUrl)}`

	// const requestURL = discordAuth + encodeURI(`?code=${parsedPayload.code}&grant_type=${grant_type}&redirect_uri=${discordRedirectUrl}`)

	logger.info('Discord Auth Call: %#v', requestURL)

	// const request = nk.httpRequest(requestURL, 'post', headers, '')
	const request = nk.httpRequest(requestURL, 'post', headers)

and I already tested all the commented lines, it always give the same error

{"body":{"error": "invalid_request", "error_description": "Missing 'code' in request."}}

What Im missing here?
The code is valid as Im generating one after each attempt and after it gives an error I the same request on postman with it and it works

After a lot of differents tries and playing with Postman I realised that I could just check the HTTP Code Snippet that postman generates.

The request URL still needs to be the same (https://discord.com/api/oauth2/token)
But the encodeURI needs to be sent on the body.

Working code snippet (if anyone needs):


const discordAuthUrl = 'https://discord.com/api/oauth2/token'

function connectDiscord(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string {
	let parsedPayload = JSON.parse(payload) as DiscordAuthPayload

	const discordClientId = ctx.env.discordClientId
	const discordClientSecretId = ctx.env.discordClientSecretId
	const discordRedirectUrl = ctx.env.discordRedirectUrl

	if (
		!discordClientId ||
		!discordClientSecretId ||
		!discordRedirectUrl ||
		discordClientId === '' ||
		discordClientSecretId === '' ||
		discordRedirectUrl === ''
	) {
		throw FormatGRPCError('Discord environment variables not set', nkruntime.Codes.INTERNAL)
	}

	const grant_type = 'authorization_code'

	const authEncoded = nk.base64Encode(`${discordClientId}:${discordClientSecretId}`)

	const headers = {
		'Authorization': `Basic ${authEncoded}`,
		'Content-Type': 'application/x-www-form-urlencoded',
	}

	logger.info('Discord Headers: %#v', headers)

	const body =
		`grant_type=${encodeURIComponent(grant_type)}` +
		`&code=${encodeURIComponent(parsedPayload.code)}` +
		`&redirect_uri=${encodeURIComponent(discordRedirectUrl)}`

	logger.info('Discord Auth Call: %#v', requestURL)

	const request = nk.httpRequest(discordAuthUrl, 'post', headers, body)

	logger.info('Discord Auth Response: %#v', request)

	if (request.code !== 200) {
		throw FormatGRPCError('Failed to connect to Discord', nkruntime.Codes.PERMISSION_DENIED)
	}

	return request.body
}