מקודד / מפענח URL
קידוד ופענוח של כתובות URL, בחירה בין רכיב בודד לכתובת שלמה, ופירוק כתובת לרכיביה.
לערך בודד כמו פרמטר query אחד. מקודד גם / ? : @ & = + כי שם הם נתונים ולא מבנה.
הפלט יופיע כאן
למה קידוד אחוזים נועד
כתובת URL רשאית להכיל רק קבוצה קטנה של תווים: האותיות A–Z ו-a–z, הספרות, קומץ סימנים כמו - . _ ~, וקבוצת תווים שמורים שנותנים לכתובת את צורתה — / ? # & = : @ ועוד אחדים. כל דבר אחר, מרווח ועד מילה בעברית ועד אמוג׳י, חייב להיות מיוצג בעקיפין. קידוד אחוזים הוא הייצוג הזה: התו מומר לבתים שלו ב-UTF-8, וכל בית נכתב כסימן אחוז ואחריו שתי ספרות הקסדצימליות. רווח הופך ל-%20, ו-א הופכת ל-%D7%90.
התווים השמורים הם המקרה המעניין. הם חוקיים בתוך כתובת, אבל יש להם משמעות מבנית: ? פותח את ה-query, & מפריד בין פרמטרים, = מפצל מפתח מערכו. כשאחד מהתווים האלה הוא חלק מהנתונים שלך ולא חלק מהמבנה, הוא חייב להיות מקודד — אחרת המפענח בצד השני יקרא אותו כסימן פיסוק ויפצל את הערך שלך במקום הלא נכון.
רכיב או כתובת שלמה — ההבחנה ששוברת קישורים
זהו המקור הנפוץ ביותר לבאגים בכתובות, ולכן הכלי הזה מבקש ממך לבחור היקף במקום לנחש.
- היקף רכיב מקודד גם את התווים המפרידים: / ? : @ & = + $ , ו-#. השתמש בו לפיסת נתונים אחת — מונח חיפוש, יעד הפניה, אסימון — שמוטמעת בתוך כתובת גדולה יותר.
- היקף כתובת שלמה משאיר את המפרידים האלה כפי שהם ומקודד רק את מה שבאמת אינו חוקי, כמו רווחים וטקסט שאינו ASCII. השתמש בו כשיש לך כתובת מלאה שכבר בנויה נכון ורק צריכה ניקוי.
טעות בכל אחד מהכיוונים שוברת משהו. קודד כתובת שלמה בהיקף רכיב וכל לוכסן וסימן שאלה יהפכו ל-%2F ול-%3F, ותקבל מחרוזת ארוכה אחת שאי אפשר להשתמש בה. קודד ערך בודד בהיקף כתובת שלמה, וסימן & בתוכו ישרוד כמפריד — כך שחיפוש אחרי "כלבים & חתולים" הופך בשקט לשני פרמטרים, והחצי השני של הערך שלך נעלם.
// הערך מכיל תו מפריד, ולכן הוא חייב escaping:
const q = 'cats & dogs'
`/search?q=${encodeURIComponent(q)}` // /search?q=cats%20%26%20dogs
`/search?q=${encodeURI(q)}` // /search?q=cats%20&%20dogs ✗
// ועדיף מכל: תן לפלטפורמה להרכיב את הכתובת
const url = new URL('https://example.com/search')
url.searchParams.set('q', 'cats & dogs')כדאי לאמץ את הגישה האחרונה כהרגל. URL ו-URLSearchParams מובנים בכל דפדפן וגם ב-Node, והם מחילים את הקידוד הנכון על כל חלק בשבילך — מה שמסלק את ההחלטה לגמרי במקום לחייב אותך לקלוע נכון ידנית בכל פעם.
למה רווח הוא לפעמים + ולפעמים %20
שני תקנים שונים פועלים כאן, והם חלוקים בדיוק לגבי תו אחד. RFC 3986, שמסדיר כתובות באופן כללי, מקודד רווח כ-%20. הפורמט הישן יותר application/x-www-form-urlencoded — זה שטפסי HTML שולחים, ולכן זה מה שרוב ה-query strings נראים כמוהו — מקודד רווח כ-+.
המלכודת היא ש-decodeURIComponent מיישם רק את הכלל הראשון. תן לו hello+world ותקבל בחזרה hello+world, על הפלוס — בלי שגיאה, בלי אזהרה, פשוט ערך שגוי באופן עדין. לכן הכלי הזה מציע את האפשרות "התייחס ל-+ כרווח", ומצביע עליה כשהקלט שלך מכיל פלוס בזמן שהאפשרות כבויה.
שים לב ש-URLSearchParams, שמפעיל את טבלת הפירוק שמתחת לכלי, פועל דווקא לפי כללי הטפסים וכן ממיר + לרווח. כלומר אותו query string עצמו יכול להתפענח אחרת תלוי באיזה API בחרת — במכוון, ובצדק, בשני המקרים.
השלכה אחת שכדאי לזכור: אם פלוס הוא באמת חלק מהנתונים שלך — מספר טלפון, ביטוי סינון — הוא חייב להיות מקודד כ-%2B. פלוס מילולי ב-query string הוא דו-משמעי במקרה הטוב, ורוב המפענחים יקראו אותו כרווח.
לקרוא כתובת
הדבק כתובת מלאה והכלי יפרק אותה לחלקיה. כדאי לדעת איך כל חלק נקרא, כי הודעות שגיאה ותיעוד מניחים שאתה מכיר את המונחים:
- סכימה — https, mailto, ftp. כל מה שלפני הנקודתיים, ומה שקובע איך לפרש את כל השאר.
- פרטי משתמש — user:password אופציונלי לפני המארח. עדיין חוקי, ועדיין רעיון רע: הם נוסעים עם כל בקשה ונוחתים בלוגים, בהיסטוריית הדפדפן ובכותרות referrer.
- מארח — שם המתחם או כתובת ה-IP. כל מה שאחריו מטופל על ידי אותו שרת, לא על ידי הרשת.
- פורט — בדרך כלל נעדר, כי 443 ל-https ו-80 ל-http מובנים מאליהם.
- מקור — סכימה, מארח ופורט יחד. זו היחידה שעליה בנויה אבטחת הדפדפן: מדיניות אותו מקור, CORS והיקף העוגיות כולם משווים מקורות, לא נתיבים.
- נתיב — החלק שאחרי המארח ולפני כל ? או #.
- Query — זוגות מפתח/ערך שאחרי ה-?, מופרדים ב-&.
- עוגן — כל מה שאחרי ה-#. באופן ייחודי, החלק הזה לעולם אינו נשלח לשרת; הדפדפן מטפל בו לבדו.
הפרט הזה על העוגן חשוב יותר משהוא נשמע. מכיוון שהוא לעולם לא עוזב את הדפדפן, כל מה שתשים אחרי # אינו נראה בלוגים של השרת — ולכן חלק מאפליקציות עמוד יחיד השתמשו בו היסטורית לניתוב, ולכן זה לא המקום לחפש בו כשמנפים בקשה שמעולם לא הגיעה.
ואזהרה אחרונה על מה שקידוד אינו עושה. קידוד אחוזים הופך טקסט לבטוח להעברה בתוך כתובת; הוא אינו סניטציה ואינו אמצעי אבטחה. קידוד ערך אינו הופך אותו לבטוח להשתלה ב-HTML, ב-SQL או בפקודת מעטפת, ופענוח קלט לא מהימן עלול לחשוף תווים — מפרידי נתיב, בתים אפסיים — שהצורה המקודדת הסתירה. אמת מה הערך הוא, בנפרד מקידוד האופן שבו הוא נוסע.
הכתובת שהקלדת אינה תמיד הכתובת שנשלחת
לפני שכתובת יוצאת לדרך היא עוברת נורמליזציה, והשכתוב שקט. הכלי מציג את התוצאה בכל פעם שהיא שונה מהקלט, מפני שאותה מחרוזת — ולא זו שהקלדת — היא שמגיעה לשרת ושמופיעה בלוג:
HTTPS://Example.COM:443\a\b הוקלד https://example.com/a/b נשלח
ארבעה כללים נפרדים פעלו שם. הסכימה והמארח יורדים לאותיות קטנות, כי אף אחד מהם אינו תלוי רישיות. הפורט מושמט כי 443 הוא ברירת המחדל ל-https. הבקסלאשים הופכים ללוכסנים קדימה, כלל תאימות שמפתיע מי שכותב נתיבים בסגנון Windows. ונתיב ריק היה הופך ללוכסן בודד. שום דבר מזה אינו שגיאה, אבל אם אתה משווה שתי כתובות או בודק כתובת מול רשימת היתר, אתה חייב להשוות את הצורות המנורמלות אחרת תקבל תשובה שגויה.
גם שם מארח עם תווים שאינם ASCII נכתב מחדש, לצורת ASCII שמתחילה ב-xn--, הנקראת punycode. הכלי מציג את שני הכיוונים: השם הקריא ואת זה שבאמת נוסע. שווה להסתכל בזה ולא לדלג, כי שני שמות יוניקוד שונים יכולים להיראות זהים לחלוטין על המסך — a לטינית ו-а קירילית הן תווים נפרדים — בעוד שהן מייצרות punycode שונה לגמרי. השוואת צורות ה-ASCII היא הדרך האמינה היחידה להבחין בין זוג כזה.
דבר אחד לעולם אינו מנורמל: פרמטר query שחוזר. הכתיבה ?tag=a&tag=b חוקית לחלוטין, והתקן אינו אומר מה משמעותה, ולכן כל מערכת בחרה תשובה משלה. PHP שומר את הערך האחרון, Express אוסף אותם למערך, ומסגרות רבות וגם URLSearchParams.get לוקחים את הראשון. הכלי מסמן מפתחות חוזרים במקום למנות אותם פעמיים בלי הערה, כי הבאג שנוצר — ערך שעובד בשירות אחד ונעלם באחר — קשה מאוד לזהות בקריאה.
שאלות נפוצות
- מה ההבדל בין היקף רכיב להיקף כתובת שלמה?
- היקף רכיב מקודד גם את התווים / ? : @ & = +, וזה נכון לערך בודד שמוטמע בתוך כתובת. היקף כתובת שלמה משאיר אותם כפי שהם, כי שם הם מפרידים בין חלקי הכתובת. שימוש בהיקף רכיב על כתובת שלמה הופך כל לוכסן ל-%2F ומייצר מחרוזת אחת בלתי שמישה.
- למה הטקסט המפוענח שלי עדיין מכיל +?
- מפני ש-decodeURIComponent פועל לפי RFC 3986, שבו + הוא פשוט פלוס. שליחת טפסים ורוב ה-query strings משתמשים בקידוד טפסים, שבו + הוא רווח. הפעל את "התייחס ל-+ כרווח" כשהטקסט הגיע מאחד מהם.
- איך מקודדים פלוס שהוא באמת פלוס?
- כותבים אותו כ-%2B. פלוס מילולי ב-query string ייקרא כרווח על ידי רוב המפענחים, ולכן כל פלוס שהוא באמת חלק מהנתונים שלך — במספר טלפון, למשל — חייב escaping.
- לקודד את כל הכתובת או רק את החלקים?
- רק את החלקים, ורצוי לא ידנית: בנה את הכתובת עם URL ו-URLSearchParams, שמחילים את הקידוד הנכון על כל רכיב. קידוד כתובת מורכבת בדיעבד הוא המקור לרוב באגי הקידוד הכפול.
- מהו קידוד כפול?
- קידוד של ערך שכבר קודד, כך ש-%20 הופך ל-%2520 — סימן האחוז עצמו עובר escaping. זה מתגלה בדרך כלל כרצפי %20 מילוליים שמופיעים בדף. פענח פעם אחת ובדוק אם אתה עדיין רואה רצפי escape; אם כן, זה קודד פעמיים.
- האם קידוד ערך הופך אותו לבטוח?
- לא. קידוד אחוזים עוסק בהעברה, לא בבטיחות. ערך מקודד הוא עדיין מה שהיה, והוא זקוק לאותה ולידציה ולאותו escaping מותאם-הקשר לפני שהוא מגיע ל-HTML, ל-SQL או למעטפת.
- למה העוגן אינו נשלח לשרת?
- זה מתוכנן: כל מה שאחרי ה-# מטופל בדפדפן בלבד ואינו מופיע בבקשה כלל. לכן אי אפשר לראות אותו בלוגים של השרת, ולכן הוא שימש היסטורית לניתוב בצד הלקוח.
- האם מה שאני מדביק נשלח לאנשהו?
- לא. הקידוד, הפענוח ופירוק הכתובת משתמשים כולם בפונקציות המובנות של הדפדפן ורצים כולם על המכשיר שלך. שום דבר שתדביק לא עוזב אותו.
- למה הפירוק מציג כתובת שונה מזו שהדבקתי?
- מפני שזו הכתובת שנשלחת. כתובת עוברת נורמליזציה לפני השימוש: הסכימה והמארח יורדים לאותיות קטנות, פורט ברירת מחדל מושמט, בקסלאשים הופכים ללוכסנים, נתיב ריק הופך ללוכסן, ומארח שאינו ASCII הופך ל-punycode. אם אתה משווה כתובות או בודק מול רשימת היתר, השווה את הצורות המנורמלות ולא את הטקסט הגולמי.
- למה לא מופיע פירוק עבור example.com/path?
- מפני שזו אינה כתובת מוחלטת — אין לה סכימה, ולכן אין מארח לזהות. הכלי לא ינחש אחת עבורך: example.com:8080 היא כבר כתובת מוחלטת תקינה שהסכימה שלה היא example.com והנתיב שלה הוא 8080, ולכן הוספה שקטה של https:// עלולה לייצר פירוק שנראה סביר והוא שגוי. הוסף את הסכימה בעצמך והחלקים יופיעו.