Ihr Filter funktioniert. Seit Monaten. Dann legt das Marketing einen Datensatz namens Newsletter #2 an, und die Anfrage kommt mit 400 zurück.
An Ihrem Code hat sich nichts geändert. In Dataverse ist nichts kaputtgegangen. Das Zeichen war es.
Hier steht, was passiert, warum die Lösung zwei Schritte hat statt einem, und welche Zeichen Microsoft für dieses Problem dokumentiert und welche stillschweigend fehlen.
Die Ausgangslage
Ein Server-Logic-Endpunkt aktualisiert Einwilligungsdatensätze. Vor dem Schreiben schlägt er einen Purpose über den Namen nach:
const filter = "msdynmkt_name eq '" + purposeName + "'";
const raw = Server.Connector.Dataverse.RetrieveMultipleRecords(
"msdynmkt_purposes", "$filter=" + filter);
Für die meisten Purposes ist das in Ordnung. Für Commercial HLC #1 - DACH kommt Status 400. Für Newsletter Q4 #2 kommt Status 400. Das Muster war eine Weile unsichtbar, weil die Testdaten sauber waren: Jeder fehlschlagende Name enthielt eine Raute.
Warum die Raute anders ist
Jedes andere Sonderzeichen in einer URL macht am Ziel Ärger. Die Raute macht Ärger, bevor die Anfrage überhaupt losgeht.
RFC 3986 definiert # als das Trennzeichen, mit dem der Fragmentbezeichner beginnt, und ist dabei genau: Der Fragmentbezeichner wird vor der Auflösung vom Rest der URI abgetrennt, und die darin enthaltene Information wird allein vom User Agent ausgewertet, unabhängig vom URI-Schema.
Abgetrennt vor der Auflösung. Der Client schneidet die URL an der Raute ab und sendet nur, was links davon steht.
Aus dieser Anfrage:
GET /api/data/v9.2/msdynmkt_purposes?$filter=msdynmkt_name eq 'Commercial HLC #1 - DACH'
wird auf dem Server das hier:
GET /api/data/v9.2/msdynmkt_purposes?$filter=msdynmkt_name eq 'Commercial HLC
Das Zeichenkettenliteral hat jetzt ein öffnendes und kein schließendes Hochkomma. Dataverse parst, scheitert, antwortet mit 400.
Das ist der laute Fehlerfall, und es ist der glückliche.
Denken Sie an Campaign #2 Berlin in einem contains()-Filter. Schneiden Sie an der Raute ab, und was übrig bleibt, hängt am genauen Aufbau. Steckt die Raute mitten in einem noch offenen Zeichenkettenliteral, dem Regelfall bei einem Wert wie diesem, bleibt auch hier ein unfertiges Hochkomma übrig, und Sie bekommen denselben lauten 400 wie oben. Ich habe genau das am 3. September 2026 live nachgestellt: ein Testkontakt mit # im Nachnamen, derselbe contains()-Filter unescaped gegen /api/data/v9.2/contacts gefahren, Ergebnis HTTP 400, "unterminated string literal", korrekt mit %23 kodiert dagegen HTTP 200 mit dem einen erwarteten Treffer. Die eigentliche Gefahr liegt woanders: Steht die Raute nicht mehr innerhalb eines offenen Literals, wenn der Client abschneidet, etwa weil im selben Query-String danach noch eine zusätzliche Bedingung oder ein $select folgt, bleibt ein für sich genommen gültiger, aber unvollständiger Filter übrig. Kein Fehler. HTTP 200. Nur eben nicht der Filter, den Sie geschrieben haben. Im Kontext von Einwilligungen oder Berechtigungen ist das deutlich schlechter als ein 400, weil nichts darauf hinweist.
Dass Ihr HTTP-Client Sie nicht schützt, hat einen Grund: Die Sache ist tatsächlich mehrdeutig. Eine Raute in einer URL hat Bedeutung. Der Client kann nicht wissen, ob Sie ein Fragment meinten oder ein Literal. Manche Bibliotheken kodieren beherzt, andere reichen Zeichenketten unverändert durch. Der Dataverse-Konnektor in Power Pages Server Logic gehört zur zweiten Sorte.
Was Microsoft dokumentiert
Mehr als man erwartet, mit einer bemerkenswerten Lücke.
Hochkommas sind dokumentiert. Die Anleitung zum Filtern in der Dataverse Web API hat einen Abschnitt "Manage single quotes": Bei einem einzelnen Wert ersetzen Sie das Hochkomma durch zwei aufeinanderfolgende Hochkommas. Die dokumentierte Fehlermeldung lohnt das Merken, weil sie die Position nennt: "There is an unterminated literal at position 21 in 'lastname eq 'O'Bryan''".
Prozentkodierung ist dokumentiert, teilweise. Dieselbe Seite sagt, dass Sie einen Zeichenkettenwert in einer Filterfunktion URL-kodieren müssen, wenn er ein Sonderzeichen enthält. Als Beispiel dient contains(name,'+123'), das scheitert, mit contains(name,'%2B123') als Lösung.
Anschließend folgt eine Tabelle der zu kodierenden Zeichen. Das ist die vollständige Liste:
| Zeichen | Kodiert |
|---|---|
| $ | %24 |
| & | %26 |
| + | %2B |
| , | %2C |
| / | %2F |
| : | %3A |
| ; | %3B |
| = | %3D |
| ? | %3F |
| @ | %40 |
Zehn Zeichen. # ist keines davon.
% selbst übrigens auch nicht, und das ist womöglich noch schlimmer. Ein Prozentzeichen in einem Wert, das nicht zu %25 kodiert wird, zerlegt jede nachfolgende Prozentgruppe. Ein Datensatz namens 50% Rabatt ist eine scharfe Granate im Filter, und die Dokumentation schweigt zu beidem.
Die zwei Zeichen, die am ehesten ohne erkennbaren Fehler scheitern, sind also genau die zwei, die in Microsofts Tabelle fehlen. Diese Lücke ist der Grund für diesen Beitrag.
Die Lösung, in der richtigen Reihenfolge
Verlockend ist ein einzelnes Replace auf die Raute. Das löst den Fall vor Ihrer Nase und lässt die anderen stehen.
Die richtige Lösung berücksichtigt, dass hier zwei verschiedene Regelebenen übereinanderliegen.
Ebene eins ist OData. Innerhalb eines Zeichenkettenliterals wird ein Hochkomma verdoppelt. So steht es in den URL Conventions von OData 4.01: Hochkommas innerhalb von Zeichenkettenliteralen werden als zwei aufeinanderfolgende Hochkommas dargestellt. Mit URLs hat das nichts zu tun.
Ebene zwei ist die URI. Reservierte Zeichen im Wert werden prozentkodiert, gemäß RFC 3986.
Wenden Sie beides in dieser Reihenfolge an, und eine Funktion erledigt alles:
function odataString(value) {
// 1. OData-Ebene: Hochkommas im Literal verdoppeln
const doubled = String(value).replace(/'/g, "''");
// 2. URI-Ebene: alles Reservierte prozentkodieren
return encodeURIComponent(doubled);
}
const filter = "msdynmkt_name eq '" + odataString(purposeName) + "'";
Die Reihenfolge ist keine Geschmacksfrage. Sie trägt, und der Grund ist ein hübscher Zufall in der JavaScript-Standardbibliothek.
encodeURIComponent kodiert alles außer A-Z a-z 0-9 - _ . ! ~ * ' ( ). Das Hochkomma steht auf der Ausnahmeliste. Die Funktion kodiert also # zu %23, % zu %25, & zu %26, + zu %2B und ? zu %3F und lässt Ihre sorgfältig verdoppelten Hochkommas exakt als '' stehen.
Andersherum überleben die verdoppelten Hochkommas zwar auch, aber Sie verlieren die Gewissheit, dass die Verdopplung auf den Originalwert angewendet wurde und nicht auf einen bereits kodierten. Erst verdoppeln. Dann kodieren. Immer.
Achten Sie darauf, was kodiert wird und was nicht. Die Hochkommas um das Literal sind Struktur, sie bleiben unkodiert. Nur der Wert läuft durch die Funktion. Wer den ganzen Filterausdruck kodiert, zerstört ihn, denn eq, die Leerzeichen und die Hochkommas sind Syntax.
Die OASIS-Spezifikation hat ein Beispiel für genau dieses Prinzip. Categories('Smartphone%2FTablet') ist gültig, Categories('Smartphone/Tablet') nicht, weil Schrägstriche als Pfadtrenner gelesen werden. Die Raute verhält sich genauso, nur eine Ebene früher.
Zwei Fallen daneben
Der Liquid-Filter url_escape ist kein Ersatz. Er ist dokumentiert als URI-Escaping für die Verwendung in einer URL, und das Beispiel in der Dokumentation macht aus 'This & that//' die Zeichenfolge This+%26+that%2F%2F. Sehen Sie sich die Leerzeichen an. Er erzeugt + im Stil der Formularkodierung. In einem OData-Zeichenkettenliteral ist + ein wörtliches Pluszeichen und kein Leerzeichen, url_escape verändert Ihren Suchbegriff also unbemerkt. Wenn Sie Filter in Liquid bauen, ist dieser Filter das falsche Werkzeug.
Achten Sie auf doppelte Kodierung. Wenn Sie Ihren bereits kodierten Filter an einen URL-Builder oder eine HTTP-Bibliothek übergeben, die Query-Parameter erneut kodiert, wird aus %23 ein %2523, und Sie sind zurück bei null Treffern. Kodieren Sie genau einmal, auf Wertebene, und wissen Sie, an welcher Stelle Ihres Stapels das passiert.
Wo Sie das trifft
Überall dort, wo Ihre Filterwerte keine GUIDs sind.
Der Klassiker sind Datensätze aus dem Marketing, weil die Raute dort Namenskonvention ist: Newsletter #1, Campaign #DACH, Commercial #2. Danach kommen generierte Kennzeichen: INV#2024-001, PO#88213. Und schließlich alles, was Benutzer eintippen und was in einem Filter landet, also Kategorien, Tags, Projektnummern und Suchfelder.
Suchfelder verdienen einen eigenen Hinweis. Dass jemand eine Raute in ein Suchfeld tippt, ist nicht exotisch, und eine Suche, die still das Falsche liefert, ist schlimmer als eine, die einen Fehler wirft.
Ein Portal, das niemand aktiv beobachtet, kann Benutzern monatelang still falsche Daten ausliefern, bevor es jemand bemerkt. Genau das fangen die monatlichen Care-Checks ab.
Die Regel, die Sie schützt, ist einfach. Ist ein Wert im Filter etwas anderes als eine GUID, läuft er durch die Escape-Funktion. Schreiben Sie odataString einmal, legen Sie es dorthin, wo das Team es findet, und verwenden Sie es überall.
Wenn Sie schon in der URL sind
Zwei dokumentierte Grenzen, die Sie kennen sollten, denn Prozentkodierung verdreifacht die Länge jedes betroffenen Zeichens ungefähr.
Microsoft dokumentiert eine maximal akzeptierte URL-Länge von 32 KB, also 32.768 Zeichen, und 64 KB innerhalb eines $batch-Body. Und es gibt eine engere Grenze, die Sie zuerst erwischt: Ein einzelnes Segment einer OData-Anfrage darf höchstens 260 Zeichen lang sein. Ein stark kodierter Wert erreicht das schneller, als man denkt.
Keine der beiden Grenzen verursacht das Rautenproblem. Beide können auftauchen, während Sie es beheben.
Was bleibt
Zwei Regeln, zwei Ebenen, eine Funktion.
Hochkommas verdoppeln, weil OData es so will. Den Wert prozentkodieren, weil die URI es so will. In dieser Reihenfolge, weil encodeURIComponent Hochkommas in Ruhe lässt und Sie sich sonst selbst täuschen könnten.
Und behalten Sie im Kopf, welche Fehler laut sind und welche leise. Der 400 vom Anfang dieses Beitrags ist die freundliche Variante. Fürchten sollten Sie den gekappten Filter, der weiterhin parst, 200 liefert und Ihnen den falschen Datensatz reicht.
Solche Fehler kosten beim ersten Mal einen Tag und danach fünf Minuten.
Weekly Coaching ist mein Festpreisformat, um genau diesen Reflex im Team zu verankern: eine feste Sitzung pro Woche, an Ihrem Code, in Ihrer Umgebung. Der Punkt ist nicht, dass ich es behebe. Der Punkt ist, dass der nächste Fall fünf Minuten dauert.
Weekly Coaching, Festpreis, ohne ÜberraschungenWeitere Beiträge
Power Pages Server Logic: 4 versteckte Grenzen
Die dokumentierte Sperrliste, vier empirische Grenzen und ein Vorgehen zum Debuggen einer undurchsichtigen Laufzeitumgebung.
Beitrag lesen → DevelopmentLiquid FetchXML vs. Web API in Power Pages
Ein Entscheidungsbaum, Performance-Vergleich und Sicherheitsmuster zwischen den beiden Datenzugriffswegen in Power Pages.
Beitrag lesen →Quellen
- Dataverse Web API, Filter rows, "Manage single quotes" und "URL encode special characters" mit der Tabelle der zehn Zeichen
- Dataverse Web API, HTTP-Anfragen erstellen und Fehler behandeln, Version v9.2, URL-Länge und 260-Zeichen-Grenze pro Segment, Fehlerhülle
- Dataverse Web API, Service Protection Limits
- RFC 3986, Abschnitt 3.5 zu Fragmenten, Abschnitt 2.2 zu reservierten Zeichen, Abschnitt 2.1 zur Prozentkodierung
- OData Version 4.01 Part 2, URL Conventions, Zeichenkettenliterale und Prozentkodierung
- MDN, encodeURIComponent und die nicht kodierten Zeichen
- Power Pages Liquid-Filter, url_escape und xml_escape
- Power Pages Liquid Template Tags, {% fetchxml %}
- Server-Objekte, Server.Connector.Dataverse