CORS using admin api from electron app (Obsidian)

Hi! I’d like to be able to use the AdminAPI client library to be able to create and edit posts from my Obsidian vault (electron-based markdown note taking app).

I am working on a plugin for Obsidian to do just this. Any user who wants to use the plugin would have to enter their own API key into the plugin settings cache.

here is my repo for the plugin, specifically the instantiation of the GhostAdminApiClient

but I am unable to POST or PUT a post to my ghost site because of a CORS error

Access to XMLHttpRequest at ‘https://{myGhostUrl}/ghost/api/admin/posts/{postId}/?source=html’ from origin ‘app://’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

I have read and understand that this admin api should not be used in a front-end client setting as to now expose the admin API key and capabilities but in this case since the user is entering their own key and not exposing it elsewhere I’m wondering if there’s a way I can work around this restriction? I haven’t found anything in my own searching…

This is a problem with a setting on the server side. Where are you hosting Ghost? I have a product that successfully makes calls from the web browser, both to self hosted and Ghost Pro, so it’s doable!

oh, that’s good to hear – I’m self-hosted!

I see I’m treading a path well-trod. I ran into the exact same issue while trying to build basically the exact same extension.

The Admin API JS client uses Axios, which in turn uses XMLHttpRequest to make requests. This is subject to CORS enforcement. Ghost isn’t returning any CORS headers, so the request fails pre-flight checks and isn’t performed.

I found the Ghost code that determines whether the CORS headers are returned. The origin header on the request must match one of the following:

  • localhost
  • The admin URL host
  • The main page URL host
  • The IP address of the server

As a test, I set admin__url: app:// in my docker-compose.yml to configure the admin URL. This obviously completely broke the admin page, which now attempts to redirect to that URL. However, it also made the server return the appropriate headers, enabling the plugin requests to work.

Would the Obsidian team consider allowing users to configure additional allowed hosts when creating custom integrations? Or is there a better approach I’m not considering?

1 Like

You could fork ghost, and change that behavior, yeah?

I mean, that’s definitely possible. But it hardly seems something worth maintaining a fork in perpetuity over.

yeah I get that it isn’t the simplest solution, though the change would be small and hopefully not too hard to keep up with upstream.

with obsidian being closed source it would probably be the fastest solution, rather than trying to petition a feature to obsidian or a ghost. I am not all that experienced in solving this type of problem though, so it’s highly possible I’m missing some idea

Had a very quick look and it seems like there are a few approaches listed in this topic Make HTTP requests from plugins - Developers & API - Obsidian Forum

The main one seems to be using Obsidian’s requestUrl method to interact with Ghost’s Admin API, it doesn’t add an Origin header so it bypasses CORS. You can either make requests to the Admin API directly or you can use the @tryghost/admin-api package and override makeRequest when passing options in so it uses requestUrl instead of axios. The default makeRequest implementation can be seen here


I appreciate the input. I hadn’t considered overriding the makeRequest implementation, but I see the SDK is designed in a way that makes that straightforward. I suspect that may make the plugin inoperable on mobile Obsidian, but I’ll explore further. Thank you!

I’m writing a plugin uploading images from obsidian to ghost. When running the code bellow it triggers CORS due to the use of axios (SDK). See also CORS using admin api from electron app (Obsidian) - #6 by subract - however I see no one who has published the code or similar to how they salvaged the issue.
To Reproduce

Code bellow and run the plugin. (Ignore if there's a parenthesis missing or something, I went back to try and reconstruct it).
	async function uploadImages(html: string) {
		// Find images that Ghost Upload supports
		let imageRegex = /!*\[\[(.*?)\]\]/g;
		let imagePromises = [];

		// Get full-path to images
		let imageDirectory: string;
		let adapter = app.vault.adapter;
		if (adapter instanceof FileSystemAdapter) {
			imageDirectory = adapter.getBasePath(); // Vault directory
			if (settings.screenshotsFolder) {
				imageDirectory = `${imageDirectory}${settings.screenshotsFolder}`;
			if (frontmatter.imageDirectory) { // Extends the image directory
				imageDirectory = `${imageDirectory}${frontmatter.imageDirectory}`;
		console.log("Image Directory", imageDirectory);
		let result: RegExpExecArray | null; // Declare the 'result' variable

		while((result = imageRegex.exec(html)) !== null) {
			let file = `${imageDirectory}/${result[1]}`;

	                // Upload the image, using the original matched filename as a reference
		                ref: file,
		                file: file

                    return Promise
	                    .then(images => {
		                    images.forEach(image => html = html.replace(image.ref, image.url));
		                    return html;
Execute the plugin in obsidian and get the following:
Access to XMLHttpRequest at 'http://localhost:2368/ghost/api/v4/admin/images/upload/' from origin 'app://' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

If you have solved this issue and if you could post the code snippets I would greatly appreciate it.