Make WordPress Core

Opened 8 years ago

Closed 3 weeks ago

#46010 closed defect (bug) (fixed)

Auto-generated term slugs can be greater than 200 characters, giving "Could not insert term into the database." error

Reported by: mikejolley Owned by: peterwilsoncc
Priority: normal Milestone: 7.2
Component: Taxonomy Version:
Severity: normal Keywords: dev-feedback has-patch 2nd-opinion has-unit-tests
Cc: Focuses:

Description

When a slug is not user defined, WordPress attempts to generate it from the term name within wp_insert_term. If the slug exists, when inserting subcategories, WordPress prefixes the slug with the top level term name.

https://core.trac.wordpress.org/browser/tags/5.0.2/src/wp-includes/taxonomy.php#L2093

Because of this, long term names can go over the slug length limit and error. WordPress admin simply shows:

Could not insert term into the database.

There is no reasoning provided, and when using wp_insert_term programatically it's hard to know what's at fault.

This issue is more apparent when using cyrillic characters since they are stored encoded in the DB.

Take "Категория на продукта" as an example. In the database this gets stored as:

%d0%ba%d0%b0%d1%82%d0%b5%d0%b3%d0%be%d1%80%d0%b8%d1%8f-%d0%bd%d0%b0-%d0%bf%d1%80%d0%be%d0%b4%d1%83%d0%ba%d1%82%d0%b0

This is 116 characters. The maximum allowed slug length defined in the wp_terms database schema is 200 characters.

Steps to reproduce:

  • Add a top level category "Категория на продукта"
  • Add another "Категория на продукта" term as a subcategory of "Категория на продукта"
  • See error

This is what WordPress attempted to insert into the database:

(
    [name] => Категория на продукта
    [slug] => %d0%ba%d0%b0%d1%82%d0%b5%d0%b3%d0%be%d1%80%d0%b8%d1%8f-%d0%bd%d0%b0-%d0%bf%d1%80%d0%be%d0%b4%d1%83%d0%ba%d1%82%d0%b0-%d0%ba%d0%b0%d1%82%d0%b5%d0%b3%d0%be%d1%80%d0%b8%d1%8f-%d0%bd%d0%b0-%d0%bf%d1%80%d0%be%d0%b4%d1%83%d0%ba%d1%82%d0%b0
    [term_group] => 0
)

The slug is too long. We either need error checking for this, or we need to truncate the slug length to avoid the issue and keep slug < 200 chars.

Attachments (3)

46010.diff​ (593 bytes ) - added by subrataemfluence 8 years ago.
term-insert-error.png​ (89.2 KB ) - added by mindctrl 14 months ago.
Term insert error when length exceeded.
trac-46010-pr-13155-playground.png​ (120.2 KB ) - added by arkaprabhachowdhury 8 weeks ago.
WordPress Playground Categories screen showing the added Cyrillic parent category and nested child with a safely truncated slug for Trac #46010 / PR #13155.

Download all attachments as: .zip

Change History (18)

#1 @swissspidy
8 years ago

  • Keywords needs-patch added

#2 @subrataemfluence
8 years ago

  • Keywords has-patch added; needs-patch removed

#3 @mikejolley
8 years ago

  • Keywords needs-patch added; has-patch removed

@subrataemfluence just a note on that diff, whilst it makes sense in technical terms, a user using cyrillic probably won't understand the length issue as they only typed 21.

I wonder if a better slug generation is needed here, or a fallback.

#4 @subrataemfluence
8 years ago

  • Keywords reporter-feedback added

While generating a slug, option is always there to truncate (let's call this the fallback) it to the maximum allowed characters.

Technically it sounds ok, but not sure whether its a good idea from SEO perspective!

When auto-generated truncate is probably the best option, apart from throwing an error and stopping it from getting added into the database.

Any suggestion on better slug generation would be nice!

#5 @mikejolley
8 years ago

Any suggestion on better slug generation would be nice!

If it cannot prefix the parent term due to length, could it not just append -2, -3 etc. Obviously it's not ideal, but it's better than the error message, and the end-user can edit it if needed manually.

Last edited 8 years ago by mikejolley (previous) (diff)

#6 @swissspidy
8 years ago

  • Keywords needs-unit-tests dev-feedback added; reporter-feedback removed

#7 @callumbw95
16 months ago

  • Keywords close added

Hi All,
I have taken a look at this now that this with the latest version of WordPress and It appears there is a string length limit in place now to stop this from erroring when inserting into the database. It no longer throughs up an error message as before, but instead just truncates behind the scenes without any notification that it has done this.
In regards to the string length due to the encoding, this still appears to be prevalent, but I really cannot think of a solution to get around this issue, and I am not sure that there will be one.
As the main complaint from this ticket looks to be resolved in the latest releases, I believe we can close this ticket down?

@mindctrl
14 months ago

Term insert error when length exceeded.

#8 @mindctrl
14 months ago

Hi all, I'm able to reproduce the issue using the same category names in the ticket description, and the error is still generic.

This ticket was mentioned in ​PR #10323 on ​WordPress/wordpress-develop by ​NikunjRajpara.


12 months ago
#9

  • Keywords has-patch added; needs-patch removed

…db insert errors (Ticket #46010)

This PR adds a truncation check to ensure that term slugs longer than 200 characters are properly shortened before insertion.
This prevents database errors that occur when creating categories, tags, or other terms with very long names.

Trac ticket: https://core.trac.wordpress.org/ticket/46010

#10 follow-up: @mindctrl
11 months ago

  • Keywords 2nd-opinion added; close removed

If taking a truncating approach, I wonder if the logic should go inside of wp_unique_term_slug() instead, which would also cover wp_update_term().

This ticket was mentioned in ​PR #13155 on ​WordPress/wordpress-develop by ​@khokansardar.


8 weeks ago
#11

  • Keywords has-unit-tests added; needs-unit-tests removed

wp_unique_term_slug() appends a parent slug or numeric suffix without
reserving room in the 200 character slug column. wp_insert_term() then
fails with a generic "Could not insert term into the database.";
wp_update_term() discards the write without reporting an error.

Non-ASCII slugs are stored percent-encoded, so this is reachable from the
Categories screen with short names in any non-Latin script — the ticket uses a
21 character Cyrillic name.

The slug is now truncated before appending, reserving room for the suffix, as
wp_unique_post_slug() already does. The helper is renamed _truncate_slug()
and moved to formatting.php since it is not post specific, with
_truncate_post_slug() kept as a silent alias. Fixing this inside
wp_unique_term_slug() covers wp_update_term() too, as suggested on the
ticket.

Trac ticket: https://core.trac.wordpress.org/ticket/46010

## Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Root-cause investigation against trunk, the patch, the unit tests, and running the PHPUnit/PHPCS/PHPStan validation. All changes were reviewed and validated by me.
and final review are mine.

#12 in reply to: ↑ 10 @khokansardar
8 weeks ago

  • Keywords needs-testing added
  • Milestone Awaiting Review → 7.2

Replying to mindctrl:

If taking a truncating approach, I wonder if the logic should go inside of wp_unique_term_slug() instead, which would also cover wp_update_term().

— that's where PR ​https://github.com/WordPress/wordpress-develop/pull/13155 puts it. Truncating inside wp_unique_term_slug() covers wp_update_term() as well as wp_insert_term().

Still reproducible on trunk (7.2-alpha) with the category names from the description: wp_unique_term_slug() returns 233 bytes for a varchar(200) column.

Two symptoms, and only one is loud:

  • wp_insert_term() — wpdb refuses the write, giving the generic "Could not insert term into the database."
  • wp_update_term() — the $wpdb->update() return is discarded at taxonomy.php:3428, so the row is silently left unwritten while the function still reports success.

More reachable than "very long names" suggests: sanitize_title() already caps slugs at 200 bytes, so on a Japanese site 22 characters fill the cap exactly — siblings 2–9 save fine and the 10th fails, because "-10" needs one byte more than "-9". Cyrillic is the same at 33 characters.

substr() isn't safe here since it can split a %d0 escape, so the patch reuses the post-side helper, renamed _truncate_slug() and moved to formatting.php as it isn't post specific. _truncate_post_slug() stays as an alias.

Supersedes 46010.diff, 46010.2.diff, fix-46010-term-slug.diff and PR 10323 — those truncate after uniquification in wp_insert_term(), which misses wp_update_term() and can shear the "-2" back off.

@arkaprabhachowdhury
8 weeks ago

WordPress Playground Categories screen showing the added Cyrillic parent category and nested child with a safely truncated slug for Trac #46010 / PR #13155.

#13 @arkaprabhachowdhury
8 weeks ago

Test Report

Tested ​PR #13155 in a fresh WordPress Playground instance.

Environment:

  • WordPress 7.2-alpha-20260819.041517
  • PHP 7.4.33
  • Chrome via WordPress Playground

Steps:
# Opened the PR build.
# Added a parent category named Категория на продукта.
# Added a same-named child under that parent.
# Repeated the collision with a 100-character Cyrillic category name.

Expected:
The child category should be created without a database error, with a safely truncated unique slug.

Results:

  • Pass: the reported Cyrillic parent/child case added successfully.
  • The child slug was 197 encoded bytes and retained valid percent-encoding.
  • The boundary case also succeeded with an encoded slug of exactly 200 bytes.
  • Core CI is green, including the API-level wp_update_term() regression coverage.

Screenshot: attachment:trac-46010-pr-13155-playground.png​

The separate 2nd-opinion/design discussion remains outside this functional result.

#14 @sajib1223
7 weeks ago

  • Keywords needs-testing removed

Patch Testing Report

Patch tested: ​https://github.com/WordPress/wordpress-develop/pull/13155

Environment

  • WordPress: trunk (7.2-alpha-63166-src)
  • PHP: 8.2.29
  • Server: nginx 1.29.5
  • Database: MySQL 8.4.8
  • Browser: n/a (WP-CLI)
  • OS: Ubuntu (WSL2)
  • Theme: Twenty Twenty-Five
  • MU Plugins: None
  • Plugins:
    • None activated

Steps taken

Reproduction on clean trunk

  1. Ran the WP-CLI script below (repro-46010.php) on clean trunk. It covers three cases: the Cyrillic parent/child from the ticket description, the "10th sibling" boundary with 22-character Japanese names, and the silent wp_update_term() path from comment:12 (slug derived from name, colliding with a 199-byte sibling slug).
  2. All three fail on clean trunk:
    1a. Parent insert: OK slug=%d0%ba%d0%b0...%d0%b0 (116 bytes)
    1b. Child insert: ERROR Could not insert term into the database.
    2.  Japanese siblings (11 inserts): FAILED -> 10: Could not insert term into the database.; 11: Could not insert term into the database.
    3.  wp_update_term(): returned success | DB name='update-target' slug=13 bytes
        ❌ SILENT FAILURE: reported success but row not updated
    
    Case 3 confirms the quiet symptom: wp_update_term() returns the term IDs as if it succeeded, but the name/slug in wp_terms are unchanged.

With patch applied

  1. Applied PR #13155 on top of the same trunk (gh pr diff 13155 | git apply, applies cleanly).
  2. Re-ran the same script:
    1a. Parent insert: OK slug=%d0%ba%d0%b0...%d0%b0 (116 bytes)
    1b. Child insert: OK slug=%d0%ba%d0%b0%d1%82...-%d0%bf%d1%80 (197 bytes)
    2.  Japanese siblings (11 inserts): all OK
    3.  wp_update_term(): returned success | DB name='бббббббббббббббббббббббббббббббббa' slug=200 bytes
        ✅ consistent
    
    The child slug is cut on a character boundary (197 bytes, no broken %xx sequence) and the wp_update_term() result now fits exactly 200 bytes with the -2 suffix intact.
  3. PHPUnit for the touched classes: Tests_Term_WpInsertTerm, Tests_Term_WpUniqueTermSlug, Tests_Term_WpUpdateTerm, Tests_Post_TruncatePostSlug, Tests_Formatting_TruncateSlug → OK (88 tests, 246 assertions).
  4. PHPCS on the changed files: no new issues (the warnings reported in taxonomy.php are pre-existing on clean trunk).
  5. Reviewed the diff: the post.php / theme-templates.php changes are only call-site renames from _truncate_post_slug() to _truncate_slug(); _truncate_post_slug() is kept as an alias, so nothing else changes behaviour.

Not covered: the Categories admin screen UI (the existing Playground report in comment:13 covers that) and the 2nd-opinion design question about where the truncation should live.

✅ Patch is solving the problem

Support Content

repro-46010.php — deletes all categories except Uncategorized first. Run with wp eval-file repro-46010.php, or from a wordpress-develop checkout (WP-CLI lives in the Docker cli container, so the path must be the container path):

cp repro-46010.php tests-local/
npm run env:cli -- eval-file /var/www/tests-local/repro-46010.php
<?php
/**
 * Repro for Trac #46010: term slugs over 200 chars fail to insert / silently fail to update.
 * Run with WP-CLI: wp eval-file repro-46010.php
 * In the wordpress-develop Docker env (no host `wp`): copy it into tests-local/ then
 *   npm run env:cli -- eval-file /var/www/tests-local/repro-46010.php
 */
global $wpdb;

// Clean up from previous runs.
foreach ( get_terms( array( 'taxonomy' => 'category', 'hide_empty' => false ) ) as $t ) {
        if ( 'uncategorized' !== $t->slug ) {
                wp_delete_term( $t->term_id, 'category' );
        }
}

echo "WP " . get_bloginfo( 'version' ) . " | PHP " . PHP_VERSION . "\n\n";

// Case 1 (from ticket): Cyrillic parent + same-named child.
$name   = 'Категория на продукта';
$parent = wp_insert_term( $name, 'category' );
echo "1a. Parent insert: " . ( is_wp_error( $parent ) ? 'ERROR ' . $parent->get_error_message() : 'OK slug=' . get_term( $parent['term_id'] )->slug . ' (' . strlen( get_term( $parent['term_id'] )->slug ) . ' bytes)' ) . "\n";

$child = wp_insert_term( $name, 'category', array( 'parent' => is_wp_error( $parent ) ? 0 : $parent['term_id'] ) );
if ( is_wp_error( $child ) ) {
        echo "1b. Child insert: ERROR " . $child->get_error_message() . "\n";
} else {
        $slug = get_term( $child['term_id'] )->slug;
        echo "1b. Child insert: OK slug=$slug (" . strlen( $slug ) . " bytes)\n";
}

// Case 2: 22-char Japanese name fills the 200-byte cap; 10th sibling needs "-10".
$jp   = str_repeat( 'あ', 22 );
$fail = array();
for ( $i = 1; $i <= 11; $i++ ) {
        $r = wp_insert_term( $jp . ' ' . $i, 'category', array( 'slug' => $jp ) );
        if ( is_wp_error( $r ) ) {
                $fail[] = "$i: " . $r->get_error_message();
        }
}
echo "2.  Japanese siblings (11 inserts): " . ( $fail ? "FAILED -> " . implode( '; ', $fail ) : 'all OK' ) . "\n";

// Case 3: wp_update_term() silent failure (slug derived from name, collides with 199-byte sibling slug).
$long = str_repeat( 'б', 33 ) . 'a'; // 33 Cyrillic chars + 'a' = 199 bytes percent-encoded.
$a    = wp_insert_term( 'update-target', 'category' );
$b    = wp_insert_term( $long, 'category' ); // takes the 199-byte slug.
$res  = wp_update_term( $a['term_id'], 'category', array( 'name' => $long, 'slug' => '' ) ); // unique slug => "-2" => 201 bytes
$row  = $wpdb->get_row( $wpdb->prepare( "SELECT name, slug FROM $wpdb->terms WHERE term_id = %d", $a['term_id'] ) );
echo "3.  wp_update_term(): " . ( is_wp_error( $res ) ? 'ERROR ' . $res->get_error_message() : 'returned success' ) . " | DB name='{$row->name}' slug=" . strlen( $row->slug ) . " bytes\n";
echo "    " . ( 'update-target' === $row->name && ! is_wp_error( $res ) ? '❌ SILENT FAILURE: reported success but row not updated' : '✅ consistent' ) . "\n";

#15 @peterwilsoncc
3 weeks ago

  • Owner set to peterwilsoncc
  • Resolution → fixed
  • Status new → closed

In 63909:

Taxonomy: Limit term slugs to 200 characters in wp_unique_term_slug().

Prevents the auto-generation of term slugs that exceed database schema's maximum of 200 characters. This prevents database errors inserting the term.

This is particularly useful for slugs generated for non-latin alphabets as the slug is stored in it's URL encoded form in the database, for example Категория на продукта generates a 116 character slug.

  • Introduces wp_truncate_slug() as a generically named function for truncating the slug of any object
  • Deprecates _truncate_post_slug() in favour of above

The test suite for _truncate_post_slug() is retained (with the addition of expecting a deprecation notice) in order to ensure that future updates do not result in errors.

Props khokansardar, arkaprabhachowdhury, callumbw95, mikejolley, mindctrl, sajib1223, subrataemfluence, swissspidy.
Fixes #46010.

Note: See TracTickets for help on using tickets.