Gå til hovedindhold

WordPress AJAX returnerer 400, fordi nonce og action ikke stemmer

Forstå hvad der faktisk skal matche

I den klassiske WordPress AJAX-model sendes en POST- eller GET-request til admin-ajax.php. Requesten skal som minimum indeholde en action-parameter, som WordPress bruger til at finde den korrekte callback via hooks som wp_ajax_mit_action_navn og eventuelt wp_ajax_nopriv_mit_action_navn for ikke-loggede brugere. Hvis action-navnet i JavaScript ikke passer præcist til suffixet i hook-navnet i PHP, bliver den forventede funktion ikke kaldt. Det giver ikke altid en sigende fejlmeddelelse, men det er en af de mest almindelige årsager til, at udviklere oplever en fejlsituation omkring AJAX i WordPress. Se den officielle gennemgang i AJAX – Plugin Handbook | Developer.WordPress.org.

Nonce-delen er en separat kontrakt. En WordPress nonce bliver typisk genereret med wp_create_nonce( ‘min_action_nonce’ ) og sendt til JavaScript via wp_localize_script() eller wp_add_inline_script() i forbindelse med korrekt enqueueing. Når requesten rammer serveren, verificeres den typisk med check_ajax_referer(). Hvis nonce er oprettet med én action-streng, men valideret med en anden, fejler kontrollen. Det samme sker, hvis JavaScript sender noncen i et felt med et andet navn end det, check_ajax_referer() forventer. Den relevante funktion er beskrevet i check_ajax_referer() – Function | Developer.WordPress.org.

Typiske symptomer på mismatch mellem nonce og action

  • Network-fanen viser status 400 fra admin-ajax.php
  • Responsen er tom, indeholder 0 eller -1, eller returnerer en generisk fejlstruktur
  • PHP-callbacken bliver aldrig ramt, selv om JavaScript sender requesten
  • Requesten virker som logget ind, men fejler som logget ud
  • Nonce findes i HTML eller JavaScript, men valideringen fejler stadig på serveren

Statuskoder og responsindhold varierer efter implementeringen. Nogle løsninger bruger wp_send_json_error() med eksplicit statuskode, mens andre lader WordPress eller egen kode dø tidligt. Derfor bør fejlsøgning tage udgangspunkt i hele kæden: enqueueing, dataeksponering til JavaScript, action-navn, nonce-oprettelse, nonce-feltnavn, hook-registrering og capability checks.

Et minimalt eksempel med fejl

Nedenfor ses et forenklet eksempel, hvor både action og nonce-validering er inkonsistente. Koden er til demonstration og bør ikke kopieres direkte til produktion uden tilpasning og test.

// PHP
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
function my_enqueue_scripts() {
	wp_enqueue_script(
		'my-ajax-script',
		get_stylesheet_directory_uri() . '/js/my-ajax.js',
		array( 'jquery' ),
		'1.0.0',
		true
	);

	wp_localize_script(
		'my-ajax-script',
		'myAjax',
		array(
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'nonce'   => wp_create_nonce( 'save_form_nonce' ),
		)
	);
}

add_action( 'wp_ajax_save_form', 'my_save_form_callback' );
function my_save_form_callback() {
	check_ajax_referer( 'submit_form_nonce', 'security' );

	wp_send_json_success( array( 'message' => 'OK' ) );
}
// JavaScript
jQuery(function ($) {
	$('#my-button').on('click', function () {
		$.post(myAjax.ajaxUrl, {
			action: 'submit_form',
			nonce: myAjax.nonce
		})
		.done(function (response) {
			console.log(response);
		})
		.fail(function (xhr) {
			console.error(xhr.responseText);
		});
	});
});

Her er der tre konkrete problemer. JavaScript sender action som submit_form, men PHP lytter på save_form. PHP validerer en nonce med action-strengen submit_form_nonce, men noncen blev oprettet med save_form_nonce. Endelig forventer check_ajax_referer() et requestfelt ved navn security, mens JavaScript sender nonce. Selv hvis ét af problemerne rettes, vil de øvrige stadig få requesten til at fejle.

Korrekt og sammenhængende implementering

Den sikreste metode er at definere action-navn og nonce-kontrakt ét sted i PHP og eksponere dem til JavaScript sammen med ajaxUrl. WordPress anbefaler korrekt enqueueing frem for at skrive værdier direkte i templates. Se Server Side PHP and Enqueuing – Plugin Handbook | Developer.WordPress.org.

// PHP
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
function my_enqueue_scripts() {
	wp_enqueue_script(
		'my-ajax-script',
		get_stylesheet_directory_uri() . '/js/my-ajax.js',
		array( 'jquery' ),
		'1.0.0',
		true
	);

	wp_localize_script(
		'my-ajax-script',
		'myAjax',
		array(
			'ajaxUrl'    => admin_url( 'admin-ajax.php' ),
			'action'     => 'save_form',
			'nonce'      => wp_create_nonce( 'save_form_nonce' ),
			'nonceField' => 'security',
		)
	);
}

add_action( 'wp_ajax_save_form', 'my_save_form_callback' );
add_action( 'wp_ajax_nopriv_save_form', 'my_save_form_callback' );
function my_save_form_callback() {
	check_ajax_referer( 'save_form_nonce', 'security' );

	if ( ! current_user_can( 'read' ) && is_user_logged_in() ) {
		wp_send_json_error( array( 'message' => 'Utilstrækkelige rettigheder.' ), 403 );
	}

	$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';

	if ( '' === $name ) {
		wp_send_json_error( array( 'message' => 'Navn mangler.' ), 400 );
	}

	wp_send_json_success( array( 'message' => 'Data modtaget.', 'name' => $name ) );
}
// JavaScript
jQuery(function ($) {
	$('#my-button').on('click', function () {
		const payload = {
			action: myAjax.action,
			name: $('#name').val()
		};

		payload[myAjax.nonceField] = myAjax.nonce;

		$.post(myAjax.ajaxUrl, payload)
			.done(function (response) {
				if (response.success) {
					console.log(response.data.message);
				} else {
					console.error(response.data.message);
				}
			})
			.fail(function (xhr) {
				console.error('HTTP-fejl:', xhr.status, xhr.responseText);
			});
	});
});

Her matcher action alle steder, nonce er oprettet og valideret med samme streng, og feltet security bruges konsekvent. Svarene returneres med wp_send_json_success() og wp_send_json_error(), som giver en mere stabil kontrakt til frontend-koden. Se også wp_send_json_success() – Function | Developer.WordPress.org og wp_send_json_error() – Function | Developer.WordPress.org.

Systematisk fejlsøgning trin for trin

  1. Kontrollér i browserens Network-fane, at requesten faktisk går til admin-ajax.php og ikke til en forkert URL.
  2. Se efter, om action-parameteren i request payload matcher det præcise hook-navn i PHP, for eksempel save_form til wp_ajax_save_form.
  3. Sammenlign nonce-action-strengen i wp_create_nonce() og check_ajax_referer(). De skal være identiske.
  4. Kontrollér feltets navn for noncen. Hvis check_ajax_referer() bruger security som andet argument, skal requesten sende security og ikke for eksempel nonce eller _ajax_nonce.
  5. Bekræft at scriptet er enqueueet på den side, hvor JavaScript kører, og at det lokaliserede objekt faktisk findes i browseren.
  6. Afgør om endpointet skal være tilgængeligt for ikke-loggede brugere. Hvis ja, mangler der ofte en wp_ajax_nopriv_-hook.
  7. Undersøg om serveren eller et sikkerhedsplugin omskriver eller blokerer requesten, især hvis fejlen kun opstår i produktion.
  8. Log midlertidigt requestdata og callback-flow i udviklingsmiljø, men undgå at logge følsomme værdier i produktion.

Hvorfor 400 ikke altid er det samme problem

En 400-status betyder overordnet, at requesten er ugyldig, men den siger ikke præcist hvorfor. Hvis du selv returnerer wp_send_json_error() med status 400 ved valideringsfejl, er det et applikationsniveau-problem. Hvis noncekontrollen fejler, kan responsen i nogle opsætninger være -1 eller en tidlig afbrydelse, som frontend blot tolker som en generisk fejl. Hvis action mangler eller er forkert, kan du også ende i et spor, hvor callbacken aldrig kører. Derfor er det vigtigt at skelne mellem HTTP-status, responsbody og om den forventede PHP-funktion overhovedet bliver udført.

Sikkerhed: nonce er ikke det samme som autorisation

En nonce beskytter primært mod uønskede eller forfalskede requests, men den siger ikke i sig selv, om brugeren må udføre handlingen. Derfor skal capability checks stadig bruges, når operationen kræver rettigheder. For loggede brugere kan current_user_can() være afgørende, og for offentlige endpoints bør inputvalidering og rate limiting overvejes i den samlede arkitektur. Dette er særligt vigtigt, hvis AJAX-callbacken opretter posts, ændrer options, skriver til databasen eller kalder tredjepartsintegrationer.

Det er også vigtigt at sanitere og validere alle inputfelter separat. sanitize_text_field() er kun korrekt til simple tekstfelter. Tal, e-mailadresser, arrays og JSON kræver andre strategier. Hvis data senere vises i admin eller frontend, skal de escapes i outputkonteksten med passende WordPress-funktioner.

Når du i virkeligheden bør bruge REST API i stedet

Mange klassiske AJAX-opgaver kan fortsat løses fint med admin-ajax.php, især i ældre temaer, metaboxes og små interaktioner. Men hvis du bygger mere moderne JavaScript-baserede grænseflader, blokke eller integrationer, kan WordPress REST API være et bedre valg. Her er ruter, metoder, permissions og fejlrespons mere eksplicitte. Autentifikation håndteres efter andre mønstre end klassiske AJAX-nonces, afhængigt af konteksten. WordPress dokumenterer dette i Authentication – REST API Handbook | Developer.WordPress.org.

I Gutenberg-orienteret udvikling vil REST API ofte passe bedre end admin-ajax.php, fordi editoren allerede arbejder tæt med WordPress’ data stores og API-lag. I klassiske temaer, child themes og page builder-scenarier som YOOtheme Pro ser man stadig ofte admin-ajax.php brugt til formularer, filtrering og mindre interaktive komponenter. Valget bør afhænge af arkitekturen og ikke af vane alene.

Særlige faldgruber i temaer, plugins og builders

  • I et child theme kan scripts blive enqueueet betinget, så den lokaliserede nonce ikke findes på alle templates.
  • I et plugin kan action-navne kollidere, hvis de er for generiske. Brug præfikserede navne.
  • I Gutenberg kan du ved en fejl blande admin-ajax og REST-mønstre i samme komponent.
  • I YOOtheme Pro eller andre builders kan markup blive gengivet asynkront eller på andre hooks end forventet, så JavaScript initialiseres før dataobjektet findes.
  • Cache eller minificering kan skjule versionsforskelle mellem PHP-output og gammel JavaScript-kode i browseren.

Praktisk tjekliste før du leder efter mere komplekse fejl

  • Brug ét præfikseret action-navn og genbrug det konsekvent i PHP og JavaScript.
  • Definér nonce-action-strengen ét sted og undgå næsten ens navne.
  • Send noncefeltet med samme nøgle, som check_ajax_referer() forventer.
  • Tilføj både wp_ajax_ og wp_ajax_nopriv_, hvis endpointet skal virke for gæster.
  • Returnér strukturerede JSON-svar med tydelige HTTP-statuskoder.
  • Læg capability checks efter noncekontrol, men før databaseændringer eller eksterne kald.
  • Test både som logget ind og logget ud, og i et miljø uden aggressive sikkerhedsregler eller cachelag.

Når WordPress AJAX returnerer 400, er den hurtigste vej til en løsning næsten altid at behandle requesten som en kontrakt mellem frontend og backend. Action-navn, nonce-action, noncefelt og hook-registrering skal hænge sammen helt ned på strengniveau. Når du samtidig bruger korrekt enqueueing, tydelige JSON-svar og eksplicit sikkerhedskontrol, bliver både fejlsøgning og vedligeholdelse væsentligt enklere over tid.

Kilder og yderligere læsning


Kategorier

Denne side er skrevet af

Simon B. H. Christensen
Tlf: 6168 3536