Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to inspect Stripe's requests #2198

Open
andrecasal opened this issue Oct 2, 2024 · 6 comments
Open

How to inspect Stripe's requests #2198

andrecasal opened this issue Oct 2, 2024 · 6 comments
Assignees
Labels

Comments

@andrecasal
Copy link

Describe the bug

I'm seeking visibility on Stripe's requests.

In this piece of code:

stripe.billingPortal.configurations.create({
	business_profile: {
		headline: `${appName} - Customer Portal`,
	},
	features: {
		customer_update: {
			enabled: true,
			allowed_updates: ['address', 'shipping', 'tax_id', 'email'],
		},
		invoice_history: { enabled: true },
		payment_method_update: { enabled: true },
		subscription_cancel: { enabled: true },
		subscription_update: {
			enabled: true,
			default_allowed_updates: ['price'],
			proration_behavior: 'always_invoice',
			products: filteredProducts,
		},
	},
})

One can't answer these questions:

  • What is the URL we’re hitting?
  • What method are we using? POST? GET?
  • What is the payload?
  • What format is the payload in? JSON? x-www-form-urlencoded?
  • What headers are we sending? How are we authenticating the request?

Some of these questions have obvious answers, but I'd much rather have full visibility:

const body = {
		business_profile: {
			headline: `${appName} - Customer Portal`,
		},
		features: {
			customer_update: {
				enabled: true,
				allowed_updates: ['address', 'shipping', 'tax_id', 'email'],
			},
			invoice_history: { enabled: true },
			payment_method_update: { enabled: true },
			subscription_cancel: { enabled: true },
			subscription_update: {
				enabled: true,
				default_allowed_updates: ['price'],
				proration_behavior: 'always_invoice',
				products: filteredProducts,
			},
		},
	}
	
	const encodedBody = encodeFormData(body)
	
	const response = await fetch(
		'https://api.stripe.com/v1/billing_portal/configurations',
		{
			method: 'POST',
			headers: {
				Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
				'Content-Type': 'application/x-www-form-urlencoded',
			},
			body: encodedBody,
		},
	)

The common solution I see online is to set logLevel: 'debug':

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2020-08-27',
  logLevel: 'debug',
})

But there is no such key on Stripe 16.12.0 or 17.0.0:
Screenshot 2024-10-02 at 11 37 49

P.S.: I realize this question may have been asked a million times before, but I couldn't find anything in old issues.

To Reproduce

N.A.

Expected behavior

N.A.

Code snippets

No response

OS

macOS

Node version

Node v20.11.0

Library version

stripe-node 17.0.0

API version

2024-09-30.acacia

Additional context

I've also tried Fiddler and Charles to intercept and log the HTTP requests with no luck.

@andrecasal andrecasal added the bug label Oct 2, 2024
@andrecasal
Copy link
Author

For anyone coming here from Google, I've figured Stripe uses the https package, so I overrode the https.request function:

import https, { type RequestOptions } from 'https'
import { URL } from 'url'
import Stripe from 'stripe'

// Store the original request function
const originalRequest = https.request

// Override the https.request function
https.request = function (
	url: string | URL | RequestOptions,
	options?: RequestOptions | ((res: any) => void),
	callback?: (res: any) => void,
) {
	// Track the body being written
	let body = ''

	// Intercept the request object to capture the body
	const req = originalRequest.apply(this, arguments as any)

	// Intercept the write function to capture the data being written (i.e., the body)
	const originalWrite = req.write
	req.write = function (chunk: any, encoding?: any, callback?: any) {
		// Accumulate the body chunks as strings
		body += chunk instanceof Buffer ? chunk.toString() : chunk
		return originalWrite.apply(req, arguments as any)
	}

	// Intercept the end function to log the body and the request details before the request is finalized
	const originalEnd = req.end
	req.end = function (chunk: any, encoding?: any, callback?: any) {
		if (chunk) {
			body += chunk instanceof Buffer ? chunk.toString() : chunk
		}
		// Log the details
		if (typeof url === 'object' && !(url instanceof URL)) {
			console.log('Request details (RequestOptions):', url)
		} else {
			console.log('Request URL:', url)
			if (options && typeof options !== 'function') {
				console.log('Request options:', options)
			}
		}
		console.log('Request body:', body)

		// Call the original end method to complete the request
		return originalEnd.apply(req, arguments as any)
	}

	return req
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
	apiVersion: '2024-09-30.acacia',
	typescript: true,
})

This now logs the entire request and give you full visibility:
Screenshot 2024-10-02 at 14 43 50

@remi-stripe remi-stripe self-assigned this Oct 2, 2024
@remi-stripe
Copy link
Contributor

remi-stripe commented Oct 2, 2024

@andrecasal We don't offer such a deep introspection today. The closest we have is to use the request and response events documented here which let you log certain information. For example you can get a JSON object that looks like this:

{
  api_version: '2024-09-30.acacia',
  idempotency_key: 'stripe-node-retry-2e06822e-2ec9-42f6-9a73-056cdef4cc18',
  method: 'POST',
  path: '/v1/customers',
  request_start_time: 1727884116263
}

This won't log our raw HTTP headers though or the raw body (since that could contain sensitive information).

I see you already found your own workaround to inspect the requests though which is neat!

I don't think we'd add deeper inspection to stripe-node or other SDKs today but that's partly because it was never asked before. Can you clarify what you were really trying to do on your end? Is this more for curiosity around how the internals of the SDK work as a one-off or is this something you need for a certain reason?

@andrecasal
Copy link
Author

Hey @remi-stripe, thanks for the feedback!

Unfortunately, the request and response events don't provide the visibility I'm looking for.

I don't think we'd add deeper inspection to stripe-node or other SDKs today but that's partly because it was never asked before.

I'm quite surprised by this. I'd expect many devs to want to swap out Stripe's lib for a native fetch request, for how much more clear it is.

Can you clarify what you were really trying to do on your end? Is this more for curiosity around how the internals of the SDK work as a one-off or is this something you need for a certain reason?

I'm building a tech stack over at launchfast.pro and one of its goals is to teach modern web dev and best practices. With all due respect for all the work put into libraries like Stripe's NodeJS library, they are great for quickly building stuff, but they do push us away from the platform.

One of the features my product (LaunchFast) provides is Offline Development. For that, we're using MSW:

import { HttpResponse, http, type HttpHandler } from 'msw'
import { requireHeader } from './utils.ts'

const { json } = HttpResponse

export const handlers: Array<HttpHandler> = [
	http.post(
		`https://api.stripe.com/v1/billing_portal/configurations`,
		async ({ request }) => {
			requireHeader(request.headers, 'Authorization')

			return json({
				// fake response
			})
		},
	),
]

To provide Mocking for Offline Development, we need to be able to answer all these questions:

  • Is it a POST or a GET?
  • What is the API endpoint?
  • What is the payload?
  • In what format is the payload?
  • Are we authenticating the request? How?

Stripe's NodeJS Library doesn't allow us to answer any of them. Hence the switch to a native fetch request:

	const body = {
		customer: customerId,
		line_items: [{ price: priceId, quantity: 1 }],
		mode: 'subscription',
		payment_method_types: ['card'],
		success_url: `${HOST_URL}/checkout`,
		cancel_url: `${HOST_URL}/plans`,
		...params,
	}

	const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded',
			Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
		},
		body: objectToQueryString(body),
	})

@remi-stripe
Copy link
Contributor

Thanks for providing extra details! I get some of your points but overall it looks like your needs are more around teaching and not around really using our SDKs. We do abstract a lot of the complexity for developers because they don't need to know whether we use JSON or form-encoded in our API or how we handle authentication or HTTP headers.

If you want to inspect the request, you can use stripe-mock which is a tool we built a few years ago to help mock our API and quickly run tests https://github.com/stripe/stripe-mock

Your switch to a raw fetch is reasonable but it means rebuilding a ton of complex logic around error handling, idempotent retries, network error and losing valuable insights into various more advanced encoding logic for example. You also seem to use a "query string" pattern which works but is not something we encourage.

I do see your specific need but I thing that my original position stands here. I don't remember this being asked before, except in rare cases where someone tried to debug something actively broken such as not passing the right parameters or a dependency mis-behaving (which we then replaced). I don't think that adding raw dumping of API requests and what happens under the hood is something that we'd add in the near future for that reason.

@andrecasal
Copy link
Author

Stripe-mock looks great, thanks for putting that on my radar 🙌

You also seem to use a "query string" pattern which works but is not something we encourage.

I'm confused. If I send JSON instead, I get this response:

Error: Stripe API error: 400 - {
  "error": {
    "message": "Invalid request (check that your POST content type is application/x-www-form-urlencoded). If you have any questions, we can help at https://support.stripe.com/.",
    "type": "invalid_request_error"
  }
}

Your switch to a raw fetch is reasonable but it means rebuilding a ton of complex logic around error handling, idempotent retries, network error and losing valuable insights into various more advanced encoding logic for example.

Please correct me if I'm wrong 🙏

  • Error handling seems just to be:
if (!response.ok) {
	const errorDetails = await response.text()
	throw new Error(`Stripe API error: ${response.status} - ${errorDetails}`)
}
  • Idempotent retries are a nice feature, but not specific to Stripe, so I'd include them as a utility.
  • network error is covered in error handling

I don't know what you mean by "losing valuable insights into various more advanced encoding logic". As far as I can tell, the benefit of the Library is that it hides this complexity—which is also why I'd like to replace it—so we wouldn't have visibility on these insights.

Thanks for this back-and-forth with me, I appreciate it 😊

@remi-stripe
Copy link
Contributor

remi-stripe commented Oct 3, 2024

I'm confused. If I send JSON instead, I get this response:

We expect application/x-www-form-urlencoded in the POST body. I likely misread your code since it say objectToQueryString(body) which I usually understand as being appending parameters to the URL itself like api.stripe.com/v1/customers?description=test or similar.

As for the second part, our SDKs will often do a lot more logic than simply returning whether the request succeeded. Our stripe-node SDK doesn't retry errors that were rate limited but many others do and we plan to also add this to stripe-node in the future. The SDK will add an idempotency key if you haven't generated one to handle network errors if you have retries enabled.
Similarly we have advanced logic around detecting null values or how to handle unsetting certain values. Not all SDKs do this but they are here to abstract and hide that complexity entirely.
We're also building new APIs where the SDKs will be in charge of detecting linked resources and potentially making extra requests to automatically expand those resources. There's definitely a cost to having "magic" in the cost, but there's also a cost to just give a raw API and let every developer deal with many of those edge-cases

I do get your points here though about wanting the SDK to be mostly a raw API request layer but that is not the goals we have for those on our end!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants